diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000000..867a790a279a5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "@babel/preset-env" + ] +} diff --git a/.docker-mongo/Dockerfile b/.docker-mongo/Dockerfile index bc250d5967130..ca963c16aadb8 100644 --- a/.docker-mongo/Dockerfile +++ b/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM rocketchat/base:8 +FROM rocketchat/base:12.14.0 ADD . /app ADD entrypoint.sh /app/bundle/ diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 1c2a93fe80773..912d99c667613 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,4 +1,4 @@ -FROM rocketchat/base:8.17.0 +FROM rocketchat/base:12.14.0 ADD . /app diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index f67bb8cf82c50..0a700be61b46c 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/rhscl/nodejs-8-rhel7 -ENV RC_VERSION 2.4.9 +ENV RC_VERSION 3.0.0 MAINTAINER buildmaster@rocket.chat diff --git a/.github/history-manual.json b/.github/history-manual.json index d3783a7b48e22..984498859c9a3 100644 --- a/.github/history-manual.json +++ b/.github/history-manual.json @@ -16,5 +16,10 @@ "title": "[BREAK] Support for Cordova (Rocket.Chat Legacy app) has reached End-of-life, support has been discontinued", "userLogin": "sampaiodiego", "contributors": [] + }], + "3.0.0-rc.9": [{ + "title": "[BREAK] TLS v1.0 and TLS v1.1 were disabled by due to NodeJS update to v12. You can still enable them by using flags like `--tls-min-v1.0` and `--tls-min-v1.1`", + "userLogin": "sampaiodiego", + "contributors": [] }] } diff --git a/.github/history.json b/.github/history.json index 2667a372e115b..afd5cd31de041 100644 --- a/.github/history.json +++ b/.github/history.json @@ -38097,6 +38097,23 @@ ], "pull_requests": [] }, + "2.3.3": { + "node_version": "8.15.1", + "npm_version": "6.9.0", + "mongo_versions": [], + "pull_requests": [ + { + "pr": "16171", + "title": "[FIX] Add missing password field back to administration area", + "userLogin": "rodrigok", + "milestone": "2.4.1", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + } + ] + }, "2.4.1": { "node_version": "8.17.0", "npm_version": "6.13.4", @@ -38106,6 +38123,17 @@ "4.0" ], "pull_requests": [ + { + "pr": "16195", + "title": "Release 2.4.1", + "userLogin": "sampaiodiego", + "contributors": [ + "mariaeduardacunha", + "sampaiodiego", + "rodrigok", + "d-gubert" + ] + }, { "pr": "16189", "title": "[FIX] Enable apps change properties of the sender on the message as before", @@ -38138,23 +38166,6 @@ } ] }, - "2.3.3": { - "node_version": "8.15.1", - "npm_version": "6.9.0", - "mongo_versions": [], - "pull_requests": [ - { - "pr": "16171", - "title": "[FIX] Add missing password field back to administration area", - "userLogin": "rodrigok", - "milestone": "2.4.1", - "contributors": [ - "rodrigok", - "sampaiodiego" - ] - } - ] - }, "2.4.2": { "node_version": "8.17.0", "npm_version": "6.13.4", @@ -38164,6 +38175,17 @@ "4.0" ], "pull_requests": [ + { + "pr": "16274", + "title": "Release 2.4.2", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "MarcosSpessatto", + "ggazzo", + "tassoevan" + ] + }, { "pr": "16147", "title": "[FIX] Setup Wizard inputs and Admin Settings", @@ -38212,6 +38234,15 @@ "4.0" ], "pull_requests": [ + { + "pr": "16358", + "title": "Release 2.4.3", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "pierre-lehnen-rc" + ] + }, { "pr": "16347", "title": "[FIX] Unknown error when sending message if 'Set a User Name to Alias in Message' setting is enabled", @@ -38280,7 +38311,16 @@ "3.6", "4.0" ], - "pull_requests": [] + "pull_requests": [ + { + "pr": "16380", + "title": "Release 2.4.5", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] }, "2.4.6": { "node_version": "8.17.0", @@ -38291,11 +38331,21 @@ "4.0" ], "pull_requests": [ + { + "pr": "16402", + "title": "Release 2.4.6", + "userLogin": "sampaiodiego", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "rodrigok" + ] + }, { "pr": "16395", "title": "Revert message properties validation", "userLogin": "MarcosSpessatto", - "milestone": "3.0.0", + "milestone": "2.4.6", "contributors": [ "MarcosSpessatto" ] @@ -38304,7 +38354,7 @@ "pr": "16401", "title": "Fix index creation for apps_logs collection", "userLogin": "rodrigok", - "milestone": "3.0.0", + "milestone": "2.4.6", "contributors": [ "rodrigok" ] @@ -38320,6 +38370,15 @@ "4.0" ], "pull_requests": [ + { + "pr": "16444", + "title": "Release 2.4.7", + "userLogin": "ggazzo", + "contributors": [ + "MarcosSpessatto", + "ggazzo" + ] + }, { "pr": "16433", "title": "[FIX] Option to make a channel default", @@ -38331,8 +38390,8 @@ } ] }, - "2.4.8": { - "node_version": "8.17.0", + "3.0.0-rc.0": { + "node_version": "12.14.0", "npm_version": "6.13.4", "mongo_versions": [ "3.4", @@ -38341,24 +38400,83 @@ ], "pull_requests": [ { - "pr": "16486", - "title": "Update presence package to 2.6.1", - "userLogin": "sampaiodiego", + "pr": "16059", + "title": "[NEW] Button to download admin server info", + "userLogin": "MarcosSpessatto", + "milestone": "3.0.0", "contributors": [ - "sampaiodiego" + "MarcosSpessatto", + "sampaiodiego", + "web-flow" ] - } - ] - }, - "2.4.9": { - "node_version": "8.17.0", - "npm_version": "6.13.4", - "mongo_versions": [ - "3.4", - "3.6", - "4.0" - ], - "pull_requests": [ + }, + { + "pr": "16465", + "title": "Revert importer streamed uploads", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "16048", + "title": "[NEW] UiKit - Interactive UI elements for Rocket.Chat Apps", + "userLogin": "d-gubert", + "contributors": [ + "ggazzo", + "sampaiodiego", + "tassoevan", + "d-gubert" + ] + }, + { + "pr": "16123", + "title": "[FIX] Result of get avatar from url can be null", + "userLogin": "MarcosSpessatto", + "milestone": "3.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "16348", + "title": "[IMPROVE] Request user presence on demand", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16279", + "title": "[IMPROVE] Major overhaul on data importers", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "16369", + "title": "[BREAK] Filter System messages per room", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "mariaeduardacunha", + "ggazzo", + "gabriellsh", + "renatobecker" + ] + }, + { + "pr": "16458", + "title": "Regression: Fix app user status change for non-existing user", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, { "pr": "16452", "title": "[FIX] `stdout` streamer infinite loop", @@ -38366,8 +38484,1267 @@ "contributors": [ "sampaiodiego" ] - } - ] + }, + { + "pr": "16451", + "title": "Regression: Fix sending a message not scrolling to bottom", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16450", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16392", + "title": "[IMPROVE] Changes App user's status when the app was enabled/disabled", + "userLogin": "lolimay", + "contributors": [ + "lolimay", + "web-flow" + ] + }, + { + "pr": "16386", + "title": "Regression: Fix sequential messages grouping", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16446", + "title": "[IMPROVE] Log as info level when Method Rate Limiters are reached", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16443", + "title": "Use GitHub Actions to store builds", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16437", + "title": "[NEW] Sort the Omnichannel Chat list according to the user preferences", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "16442", + "title": "Regression: recent opened rooms being marked as read", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16433", + "title": "[FIX] Option to make a channel default", + "userLogin": "MarcosSpessatto", + "milestone": "2.4.7", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "16065", + "title": "[NEW] Setting to only send plain text emails", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16438", + "title": "Regression: Fix status bar margins", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16425", + "title": "[NEW] Check the Omnichannel service status per Department", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker", + "lolimay" + ] + }, + { + "pr": "16414", + "title": "[IMPROVE] Show more information related to the Omnichannel room closing data", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "16401", + "title": "Fix index creation for apps_logs collection", + "userLogin": "rodrigok", + "milestone": "2.4.6", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16395", + "title": "Revert message properties validation", + "userLogin": "MarcosSpessatto", + "milestone": "2.4.6", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "16397", + "title": "[FIX] Rooms not being marked as read sometimes", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "16398", + "title": "Update apps engine to 1.12.0-beta.2496", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + null + ] + }, + { + "pr": "16388", + "title": "[FIX] Container heights", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16360", + "title": "Regression: App deletion wasn’t returning the correct information", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16351", + "title": "[BREAK] Remove deprecated publications", + "userLogin": "MarcosSpessatto", + "milestone": "3.0.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "16393", + "title": "[IMPROVE] Update katex version", + "userLogin": "MartinSchoeler", + "milestone": "3.0.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "16263", + "title": "[FIX] Mail Msg Cancel button not closing the flexbar", + "userLogin": "ashwaniYDV", + "milestone": "3.0.0", + "contributors": [ + "ashwaniYDV" + ] + }, + { + "pr": "16378", + "title": "[FIX] Highlight freezing the UI", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "16243", + "title": "[BREAK] Hide system messages", + "userLogin": "mariaeduardacunha", + "milestone": "3.0.0", + "contributors": [ + "mariaeduardacunha", + "ggazzo", + "gabriellsh", + "renatobecker" + ] + }, + { + "pr": "16373", + "title": "[IMPROVE] Prevent \"App user\" from being deleted by the admin", + "userLogin": "lolimay", + "contributors": [ + "lolimay", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "16375", + "title": "[FIX] Adding 'lang' tag", + "userLogin": "mariaeduardacunha", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16114", + "title": "Lint: Resolve complexity warnings", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16379", + "title": "Fix Preview Docker image build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16252", + "title": "[BREAK] Upgrade to Meteor 1.9 and NodeJS 12", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16362", + "title": "[FIX] App removal was moving logs to the trash collection", + "userLogin": "rodrigok", + "milestone": "2.4.4", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16356", + "title": "[FIX] Role tags missing - Description field explanation", + "userLogin": "mariaeduardacunha", + "milestone": "3.0.0", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16361", + "title": "Regression: Rate limiter was not working due to Meteor internal changes", + "userLogin": "rodrigok", + "milestone": "2.4.4", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16176", + "title": "[FIX] Invite links usage by channel owners/moderators", + "userLogin": "pierre-lehnen-rc", + "milestone": "2.4.3", + "contributors": [ + "pierre-lehnen-rc", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "16352", + "title": "Fix assets download on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16347", + "title": "[FIX] Unknown error when sending message if 'Set a User Name to Alias in Message' setting is enabled", + "userLogin": "sampaiodiego", + "milestone": "2.4.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16237", + "title": "Send build artifacts to S3", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "15896", + "title": "[NEW] Create a user for the Apps during installation", + "userLogin": "lolimay", + "contributors": [ + "Cool-fire", + "lolimay", + "d-gubert" + ] + }, + { + "pr": "16150", + "title": "Add missing translations", + "userLogin": "ritwizsinha", + "contributors": [ + "ritwizsinha" + ] + }, + { + "pr": "16253", + "title": "[FIX] Slack CSV User Importer", + "userLogin": "ggazzo", + "milestone": "2.4.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16142", + "title": "[FIX] The \"click to load\" text is hard-coded and not translated.", + "userLogin": "ashwaniYDV", + "milestone": "3.0.0", + "contributors": [ + "ashwaniYDV" + ] + }, + { + "pr": "16233", + "title": "[FIX] Integrations list without pagination and outgoing integration creation", + "userLogin": "MarcosSpessatto", + "milestone": "2.4.2", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "16147", + "title": "[FIX] Setup Wizard inputs and Admin Settings", + "userLogin": "tassoevan", + "milestone": "2.4.2", + "contributors": [ + "tassoevan", + "ggazzo" + ] + }, + { + "pr": "16234", + "title": "[FIX] FileUpload.getBuffer was not working through the Apps-Engine", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "16183", + "title": "[FIX] Integrations admin page", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16197", + "title": "[FIX]Readme Help wanted section", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16181", + "title": "[IMPROVE] Improve function to check if setting has changed", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "15840", + "title": "[NEW] Add GUI for customFields in Omnichannel conversations", + "userLogin": "antkaz", + "milestone": "3.0.0", + "contributors": [ + "antkaz" + ] + }, + { + "pr": "16189", + "title": "[FIX] Enable apps change properties of the sender on the message as before", + "userLogin": "d-gubert", + "milestone": "2.4.1", + "contributors": [ + "d-gubert", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16184", + "title": "[FIX] User stuck after reset password", + "userLogin": "sampaiodiego", + "milestone": "2.4.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16171", + "title": "[FIX] Add missing password field back to administration area", + "userLogin": "rodrigok", + "milestone": "2.4.1", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "16167", + "title": "[NEW] update on mongo, node and caddy on snap", + "userLogin": "LuluGO", + "contributors": [ + "LuluGO" + ] + }, + { + "pr": "16063", + "title": "[NEW] Enforce plain text emails converting from HTML when no text version supplied", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16121", + "title": "[IMPROVE] Status Text form validation", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "15768", + "title": "[FIX] auto translate cache", + "userLogin": "vickyokrm", + "milestone": "3.0.0", + "contributors": [ + "vickyokrm" + ] + }, + { + "pr": "16060", + "title": "[FIX] Save password without confirmation", + "userLogin": "ashwaniYDV", + "milestone": "3.0.0", + "contributors": [ + "ashwaniYDV" + ] + }, + { + "pr": "16141", + "title": "Disable PR Docker image build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "15913", + "title": "[IMPROVE] Set the color of the cancel button on modals to #bdbebf for enhanced visibiity", + "userLogin": "ritwizsinha", + "milestone": "3.0.0", + "contributors": [ + "ritwizsinha" + ] + }, + { + "pr": "16039", + "title": "[FIX] Break message-attachment text to the next line", + "userLogin": "ritwizsinha", + "milestone": "3.0.0", + "contributors": [ + "ritwizsinha" + ] + }, + { + "pr": "16071", + "title": "[IMPROVE] Remove NRR", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16122", + "title": "Add Cloud Info to translation dictionary", + "userLogin": "aviral243", + "contributors": [ + "aviral243" + ] + }, + { + "pr": "16139", + "title": "[FIX] JS errors on Administration page", + "userLogin": "mariaeduardacunha", + "milestone": "2.4.1", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16108", + "title": "[FIX] SafePorts: Ports 80, 8080 & 443 linked to respective protocols (#16108)", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh", + "ggazzo" + ] + }, + { + "pr": "16049", + "title": "[FIX] Drag and drop disabled when file upload is disabled", + "userLogin": "mariaeduardacunha", + "milestone": "3.0.0", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16113", + "title": "[FIX] Video message sent to wrong room", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16047", + "title": "[FIX] \"User not found\" for direct messages", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16036", + "title": "[BREAK] Removed room counter from sidebar", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16051", + "title": "[FIX] Embedded style when using 'go' command", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16064", + "title": "[NEW] Setting Top navbar in embedded mode ", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16083", + "title": "[FIX] Thread message icon overlapping text", + "userLogin": "gabriellsh", + "milestone": "3.0.0", + "contributors": [ + "gabriellsh", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "16085", + "title": "[FIX] Login change language button", + "userLogin": "mariaeduardacunha", + "milestone": "3.0.0", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16080", + "title": "[FIX] api-bypass-rate-limiter permission was not working", + "userLogin": "MarcosSpessatto", + "milestone": "3.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "16107", + "title": "Merge master into develop & Set version to 2.5.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "16444", + "title": "Release 2.4.7", + "userLogin": "ggazzo", + "contributors": [ + "MarcosSpessatto", + "ggazzo" + ] + }, + { + "pr": "16377", + "title": "Release 2.4.4", + "userLogin": "sampaiodiego", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "16189", + "title": "[FIX] Enable apps change properties of the sender on the message as before", + "userLogin": "d-gubert", + "milestone": "2.4.1", + "contributors": [ + "d-gubert", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16171", + "title": "[FIX] Add missing password field back to administration area", + "userLogin": "rodrigok", + "milestone": "2.4.1", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "16139", + "title": "[FIX] JS errors on Administration page", + "userLogin": "mariaeduardacunha", + "milestone": "2.4.1", + "contributors": [ + "mariaeduardacunha" + ] + } + ] + }, + "3.0.0-rc.1": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16469", + "title": "Fix tests", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "3.0.0-rc.2": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16488", + "title": "Regression: prevent submit modal", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16486", + "title": "Update presence package to 2.6.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16484", + "title": "[FIX] Missing edited icon in newly created messages", + "userLogin": "MartinSchoeler", + "milestone": "3.0.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "16483", + "title": "Regression: allow private channels to hide system messages", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16475", + "title": "Regression: Fix uikit modal closing on click", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16477", + "title": "Regression: Fix undefined presence after reconnect", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16473", + "title": "[FIX] Read Message after receive a message and the room is opened", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16474", + "title": "[FIX] Send message with pending messages", + "userLogin": "ggazzo", + "milestone": "3.0.0", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "3.0.0-rc.3": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16504", + "title": "Remove users.info being called without need", + "userLogin": "sampaiodiego", + "milestone": "3.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16503", + "title": "Add Ui Kit container", + "userLogin": "sampaiodiego", + "milestone": "3.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16494", + "title": "Catch zip errors on import file load", + "userLogin": "rodrigok", + "milestone": "3.0.0", + "contributors": [ + "rodrigok" + ] + } + ] + }, + "3.0.0-rc.4": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16505", + "title": "Fix: License missing from manual register handler", + "userLogin": "geekgonecrazy", + "milestone": "3.0.0", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "16489", + "title": "Exclude federated and app users from active user count", + "userLogin": "d-gubert", + "milestone": "3.0.0", + "contributors": [ + "d-gubert" + ] + } + ] + }, + "2.4.8": { + "node_version": "8.17.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16506", + "title": "Release 2.4.8", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16486", + "title": "Update presence package to 2.6.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "3.0.0-rc.5": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16515", + "title": "Regression: Update Uikit", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "d-gubert" + ] + }, + { + "pr": "16514", + "title": "Regression: UIKit - Send container info on block actions triggered on a message", + "userLogin": "d-gubert", + "milestone": "3.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "16516", + "title": "Use base64 for import files upload to prevent file corruption", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16511", + "title": "Regression: Send app info along with interaction payload to the UI", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "16513", + "title": "Regression: Ui Kit messaging issues (#16513)", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "3.0.0-rc.6": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16528", + "title": "Regression: update package-lock", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "3.0.0-rc.7": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16522", + "title": "[BREAK] Change apps/icon endpoint to return app's icon and use it to show on Ui Kit modal", + "userLogin": "sampaiodiego", + "milestone": "3.0.0", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "2.4.9": { + "node_version": "8.17.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16544", + "title": "Release 2.4.9", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16452", + "title": "[FIX] `stdout` streamer infinite loop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "3.0.0-rc.8": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16552", + "title": "Regression: UIkit input states", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16547", + "title": "[FIX] Do not stop on DM imports if one of users was not found", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16467", + "title": "[FIX] Introduce AppLivechatBridge.isOnlineAsync method", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker", + "d-gubert" + ] + }, + { + "pr": "16540", + "title": "Regression: UIKit missing select states: error/disabled", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "3.0.0-rc.9": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16544", + "title": "Release 2.4.9", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16377", + "title": "Release 2.4.4", + "userLogin": "sampaiodiego", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "16189", + "title": "[FIX] Enable apps change properties of the sender on the message as before", + "userLogin": "d-gubert", + "milestone": "2.4.1", + "contributors": [ + "d-gubert", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "16171", + "title": "[FIX] Add missing password field back to administration area", + "userLogin": "rodrigok", + "milestone": "2.4.1", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "16139", + "title": "[FIX] JS errors on Administration page", + "userLogin": "mariaeduardacunha", + "milestone": "2.4.1", + "contributors": [ + "mariaeduardacunha" + ] + }, + { + "pr": "16562", + "title": "Regression: fix read unread messages", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "16570", + "title": "Regression: UIKit update modal actions", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "16578", + "title": "[FIX] When copying invite links, multiple toastr messages", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16580", + "title": "[FIX] Livechat Widget version 1.3.1", + "userLogin": "renatobecker", + "milestone": "3.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "16584", + "title": "Update Apps-Engine version", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "16571", + "title": "[FIX] Error when successfully joining room by invite link", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "16575", + "title": "Add breaking notice regarding TLS", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16581", + "title": "[FIX] Invite links proxy URLs not working when using CDN", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "16556", + "title": "Regression: Modal onSubmit", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + } + ] + }, + "3.0.0-rc.10": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16601", + "title": "[FIX] Bug on starting Jitsi video calls , multiple messages", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + } + ] + }, + "3.0.0-rc.11": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "16607", + "title": "Regression: send file modal not working via keyboard", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "16521", + "title": "Fix github actions accessing the github registry", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "web-flow" + ] + } + ] + }, + "3.0.0": { + "node_version": "12.14.0", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [] } } -} +} \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 4a17c94278200..9a9f943d9e04b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -29,10 +29,10 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat $GITHUB_EVENT_PATH - - name: Use Node.js 8.17.0 + - name: Use Node.js 12.14.0 uses: actions/setup-node@v1 with: - node-version: "8.17.0" + node-version: "12.14.0" - uses: actions/checkout@v1 @@ -40,24 +40,32 @@ jobs: run: | npx package-lock-check + - name: Cache cypress + id: cache-cypress + uses: actions/cache@v1 + with: + path: /home/runner/.cache/Cypress + key: ${{ runner.OS }}-cache-cypress-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} + - name: Cache node modules + if: steps.cache-cypress.outputs.cache-hit == 'true' id: cache-nodemodules uses: actions/cache@v1 with: path: node_modules - key: ${{ runner.OS }}-node_modules-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.OS }}-node_modules-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: Cache meteor local uses: actions/cache@v1 with: path: ./.meteor/local - key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions') }} + key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: Cache meteor uses: actions/cache@v1 with: path: ~/.meteor - key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release') }} + key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: Install Meteor run: | @@ -88,7 +96,7 @@ jobs: git version - name: npm install - if: steps.cache-nodemodules.outputs.cache-hit != 'true' + if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' run: | meteor npm install @@ -156,7 +164,7 @@ jobs: strategy: matrix: - node-version: ["8.17.0"] + node-version: ["12.14.0"] mongodb-version: ["3.4", "3.6", "4.0"] steps: @@ -193,15 +201,23 @@ jobs: - uses: actions/checkout@v1 + - name: Cache cypress + id: cache-cypress + uses: actions/cache@v1 + with: + path: /home/runner/.cache/Cypress + key: ${{ runner.OS }}-cache-cypress-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} + - name: Cache node modules + if: steps.cache-cypress.outputs.cache-hit == 'true' id: cache-nodemodules uses: actions/cache@v1 with: path: node_modules - key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: NPM install - if: steps.cache-nodemodules.outputs.cache-hit != 'true' + if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' run: | npm install @@ -229,7 +245,7 @@ jobs: build-image-pr: runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: github.event_name == 'DEACTIVATED-pull_request' steps: - uses: actions/checkout@v1 @@ -239,24 +255,24 @@ jobs: uses: actions/cache@v1 with: path: node_modules - key: ${{ runner.OS }}-node_modules-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.OS }}-node_modules-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: Cache meteor local uses: actions/cache@v1 with: path: ./.meteor/local - key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions') }} + key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - name: Cache meteor uses: actions/cache@v1 with: path: ~/.meteor - key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release') }} + key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.github/workflows/build_and_test.yml') }} - - name: Use Node.js 8.17.0 + - name: Use Node.js 12.14.0 uses: actions/setup-node@v1 with: - node-version: "8.17.0" + node-version: "12.14.0" - name: Install Meteor run: | @@ -313,9 +329,7 @@ jobs: run: | cd /tmp/build-pr - export OWNER="${GITHUB_REPOSITORY%/*}" - - docker login docker.pkg.github.com -u "${OWNER}" -p "${GITHUB_TOKEN}" + docker login docker.pkg.github.com -u "${GITHUB_ACTOR}" -p "${GITHUB_TOKEN}" cp $GITHUB_WORKSPACE/.docker/Dockerfile . diff --git a/.gitignore b/.gitignore index af1cd3d520343..f68d665bf8e81 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ tests/end-to-end/temporary_staged_test .screenshots /private/livechat /storybook-static +/tests/cypress/screenshots \ No newline at end of file diff --git a/.meteor/packages b/.meteor/packages index 78743cc071182..73bb4024a7dfc 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -16,9 +16,9 @@ check@1.3.1 ddp-rate-limiter@1.0.7 ddp-common@1.4.0 dynamic-import@0.5.1 -ecmascript@0.13.2 -typescript@3.7.1 -ejson@1.1.0 +ecmascript@0.14.0 +typescript@3.7.2 +ejson@1.1.1 email@1.2.3 fastclick@1.0.13 http@1.4.2 @@ -26,7 +26,7 @@ jquery@1.11.10 logging@1.1.20 meteor-base@1.4.0 mobile-experience@1.0.5 -mongo@1.7.0 +mongo@1.8.0 random@1.1.0 rate-limit@1.0.9 reactive-dict@1.3.0 @@ -36,7 +36,7 @@ service-configuration@1.0.11 session@1.2.0 shell-server@0.4.0 spacebars -standard-minifier-js@2.5.2 +standard-minifier-js@2.6.0 tracker@1.2.0 #rocketchat:google-natural-language @@ -77,10 +77,10 @@ littledata:synced-cron edgee:slingshot jalik:ufs-local@0.2.5 -accounts-base@1.4.5 +accounts-base@1.5.0 accounts-oauth@1.1.16 autoupdate@1.6.0 -babel-compiler@7.4.2 +babel-compiler@7.5.0 google-oauth@1.2.6 htmljs less @@ -92,7 +92,7 @@ raix:eventemitter routepolicy@1.1.0 sha@1.0.9 templating -webapp@1.7.5 +webapp@1.8.0 webapp-hashing@1.0.9 rocketchat:oauth2-server rocketchat:i18n diff --git a/.meteor/release b/.meteor/release index bfccdc2c96b3b..c6ae8ec13c5f0 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.8.3 +METEOR@1.9 diff --git a/.meteor/versions b/.meteor/versions index e28c63138e0a9..ec835fc75d81c 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,16 +1,16 @@ -accounts-base@1.4.5 +accounts-base@1.5.0 accounts-facebook@1.3.2 accounts-github@1.4.2 accounts-google@1.3.3 accounts-meteor-developer@1.4.2 accounts-oauth@1.1.16 -accounts-password@1.5.2 +accounts-password@1.5.3 accounts-twitter@1.4.2 aldeed:simple-schema@1.5.4 allow-deny@1.1.0 autoupdate@1.6.0 -babel-compiler@7.4.2 -babel-runtime@1.4.0 +babel-compiler@7.5.0 +babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 blaze@2.3.4 @@ -19,7 +19,7 @@ blaze-tools@1.0.10 boilerplate-generator@1.6.0 caching-compiler@1.2.1 caching-html-compiler@1.1.3 -callback-hook@1.2.0 +callback-hook@1.3.0 cfs:http-methods@0.0.32 check@1.3.1 coffeescript@1.0.17 @@ -34,10 +34,10 @@ deps@1.0.12 diff-sequence@1.1.1 dispatch:run-as-user@1.1.1 dynamic-import@0.5.1 -ecmascript@0.13.2 +ecmascript@0.14.0 ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.9.0 -ecmascript-runtime-server@0.8.0 +ecmascript-runtime-client@0.10.0 +ecmascript-runtime-server@0.9.0 edgee:slingshot@0.7.1 ejson@1.1.1 email@1.2.3 @@ -83,16 +83,16 @@ meteor-base@1.4.0 meteor-developer-oauth@1.2.1 meteorhacks:inject-initial@1.0.4 meteorspark:util@0.2.0 -minifier-css@1.4.3 -minifier-js@2.5.1 +minifier-css@1.5.0 +minifier-js@2.6.0 minimongo@1.4.5 mizzao:timesync@0.3.4 mobile-experience@1.0.5 mobile-status-bar@1.0.14 -modern-browsers@0.1.4 -modules@0.14.0 -modules-runtime@0.11.0 -mongo@1.7.0 +modern-browsers@0.1.5 +modules@0.15.0 +modules-runtime@0.12.0 +mongo@1.8.0 mongo-decimal@0.1.1 mongo-dev-server@1.1.0 mongo-id@1.0.7 @@ -102,7 +102,7 @@ mystor:device-detection@0.2.0 nimble:restivus@0.8.12 nooitaf:colors@1.1.2_1 npm-bcrypt@0.9.3 -npm-mongo@3.2.0 +npm-mongo@3.3.0 oauth@1.2.8 oauth1@1.2.2 oauth2@1.2.1 @@ -140,7 +140,7 @@ socket-stream-client@0.2.2 spacebars@1.0.15 spacebars-compiler@1.1.3 srp@1.0.12 -standard-minifier-js@2.5.2 +standard-minifier-js@2.6.0 templating@1.3.2 templating-compiler@1.3.3 templating-runtime@1.3.2 @@ -149,9 +149,9 @@ tmeasday:check-npm-versions@0.3.2 todda00:friendly-slugs@0.6.0 tracker@1.2.0 twitter-oauth@1.2.0 -typescript@3.7.1 +typescript@3.7.2 ui@1.0.13 underscore@1.0.10 url@1.2.0 -webapp@1.7.5 +webapp@1.8.0 webapp-hashing@1.0.9 diff --git a/.scripts/start.js b/.scripts/start.js index bbc7f551c504c..aa9e3b3347570 100644 --- a/.scripts/start.js +++ b/.scripts/start.js @@ -112,7 +112,7 @@ function startChimp() { startProcess({ name: 'Chimp', command: 'npm', - params: ['run', 'chimp-test'], + params: ['run', 'testci'], // command: 'exit', // params: ['2'], options: { diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index 0f54d5842f9c0..a016135c0a3ab 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -29,4 +29,6 @@ execstack --clear-execstack npm/node_modules/sharp/vendor/lib/librsvg-2.so* # Having to manually remove because of latest warning rm -rf npm/node_modules/meteor/konecty_user-presence/node_modules/colors/lib/.colors.js.swp rm -rf node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp +rm -rf npm/node_modules/sharp/node_modules/semver/bin/.semver.js.swp +rm -rf npm/node_modules/tar/lib/.mkdir.js.swp diff --git a/.snapcraft/resources/preparecaddy b/.snapcraft/resources/preparecaddy index a353dbf88542f..2bb1df13baa91 100755 --- a/.snapcraft/resources/preparecaddy +++ b/.snapcraft/resources/preparecaddy @@ -1,6 +1,6 @@ #! /bin/bash -caddy_version="v0.11.0" +caddy_version="v1.0.4" caddy_bin="caddy" caddy_dl_ext=".tar.gz" diff --git a/.snapcraft/resources/preparemongo b/.snapcraft/resources/preparemongo index 9cfe098409ecc..3ccfd8b35195c 100755 --- a/.snapcraft/resources/preparemongo +++ b/.snapcraft/resources/preparemongo @@ -2,8 +2,8 @@ if [[ $(uname -m) == "x86_64" ]] then - wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.20.tgz" - tar -zxf ./mongodb-linux-x86_64-3.4.20.tgz --strip-components=1 + wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.6.14.tgz" + tar -zxf ./mongodb-linux-x86_64-3.6.14.tgz --strip-components=1 else IFS=" " read -a links <<< $(apt-get -y --print-uris install mongodb | egrep -o "https?://[^']+") for link in ${links[@]} diff --git a/.snapcraft/resources/preparenode b/.snapcraft/resources/preparenode index 9490792ba201e..e050315db48f6 100755 --- a/.snapcraft/resources/preparenode +++ b/.snapcraft/resources/preparenode @@ -1,6 +1,6 @@ #!/bin/bash -node_version="v8.17.0" +node_version="v12.14.0" unamem="$(uname -m)" if [[ $unamem == *aarch64* ]]; then diff --git a/.snapcraft/resources/startRocketChat b/.snapcraft/resources/startRocketChat index 35898d0e3152e..7e1cca2f07870 100755 --- a/.snapcraft/resources/startRocketChat +++ b/.snapcraft/resources/startRocketChat @@ -4,6 +4,14 @@ function start_rocketchat { echo "Checking if oplog has been enabled, and enabling if not" LC_ALL=C mongo $SNAP/bin/initreplset.js + echo "Checking if mongo featureCompatibilityVersion is correct, changing if not" + db_version=$(mongo --eval "printjson(db.version())" |tail -1 |tr -d '"' |cut -d. -f1,2) + db_comp_version=$(mongo --eval "printjson(db.adminCommand ({getParameter: 1, featureCompatibilityVersion: 1}))"|grep '"version" :' |cut -d: -f3 |cut -d} -f1|tr -d '[:space:]'|tr -d '"') + db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" + if [[ $db_version == "3.6" ]] && [[ $db_comp_version == "3.4" ]] && [[ $db_featureCompatibilityVersion == "3.6" ]]; then + LC_ALL=C mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.6\" }))" + fi + ## For making fonts work for sharp export XDG_DATA_HOME=$SNAP/usr/share @@ -41,6 +49,11 @@ try_times=0 function try_start { + refreshing="$(snapctl get snap-refreshing)" + if [ $refreshing == "true" ]; then + exit 0 + fi + search=$(ps --pid $(cat $SNAP_COMMON/mongod.pid) -o comm=) if [ $search ] diff --git a/.snapcraft/snap/hooks/configure b/.snapcraft/snap/hooks/configure index 3f2a606e7f11a..3a2863d7de16f 100755 --- a/.snapcraft/snap/hooks/configure +++ b/.snapcraft/snap/hooks/configure @@ -120,3 +120,26 @@ else fi fi +# Obtain backup value +backup="$(snapctl get backup-on-refresh)" +# Validate it +backup_regex='((enable)|(disable))' +if ! [[ $backup =~ $backup_regex ]] ; then + echo "\"$backup\" should be enable or disable" >&2 + exit 1 +fi + +# Obtain db featureCompatibilityVersion +db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" +db_featureCompatibilityVersion_regex='^3\.[4,6]$' +if ! [[ $db_featureCompatibilityVersion =~ $db_featureCompatibilityVersion_regex ]] ; then + echo "\"$db_featureCompatibilityVersion\" should be 3.4 or 3.6" >&2 + exit 1 +else + db_version=$(mongo --eval "printjson(db.version())" |tail -1 |tr -d '"' |cut -d. -f1,2) + if [[ $db_version == "3.6" ]] && [[ $db_featureCompatibilityVersion == "3.4" ]]; then + mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.4\" }))" + elif [[ $db_version == "3.6" ]] && [[ $db_featureCompatibilityVersion == "3.6" ]]; then + mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.6\" }))" + fi +fi diff --git a/.snapcraft/snap/hooks/install b/.snapcraft/snap/hooks/install index 9a78145595ec7..c7ed881d4b82d 100755 --- a/.snapcraft/snap/hooks/install +++ b/.snapcraft/snap/hooks/install @@ -14,3 +14,13 @@ snapctl set mongo-oplog-url=mongodb://localhost:27017/local # Initialize the protocol to a default snapctl set https=disable + +# Initialize backup to a default +snapctl set backup-on-refresh=disable + +# Initialize snap-refreshing to false +snapctl set snap-refreshing=false + +# Initialize db-feature-compatibility-version to 3.6 +snapctl set db-feature-compatibility-version=3.6 + diff --git a/.snapcraft/snap/hooks/post-refresh b/.snapcraft/snap/hooks/post-refresh index f4172f2dd0642..ec0a988afd4c7 100755 --- a/.snapcraft/snap/hooks/post-refresh +++ b/.snapcraft/snap/hooks/post-refresh @@ -30,3 +30,18 @@ if [ -z "$https" ]; then snapctl set https=disable fi +# Initialize backup to a default +backup="$(snapctl get backup-on-refresh)" +if [ -z "$backup" ]; then + snapctl set backup-on-refresh=disable +fi + +# Set back snap-refreshing to false +snapctl set snap-refreshing=false + +# Initialize db-feature-compatibility-version to a default +db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" +if [ -z "$db_featureCompatibilityVersion" ]; then + snapctl set db-feature-compatibility-version=3.6 +fi + diff --git a/.snapcraft/snap/hooks/pre-refresh b/.snapcraft/snap/hooks/pre-refresh new file mode 100755 index 0000000000000..41d8df57a1e89 --- /dev/null +++ b/.snapcraft/snap/hooks/pre-refresh @@ -0,0 +1,25 @@ +#!/bin/bash + +export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu" +export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH + +TIMESTAMP=$(date +"%Y%m%d.%H%M") + +# stop rocketchat +snapctl set snap-refreshing=true + +rocketchatpid=$(pgrep -f "node $SNAP/main.js") +kill -9 $rocketchatpid +if ! [ $? == 0 ]; then + echo "Failed to stop rocketchat service" > $SNAP_COMMON/refresh_${TIMESTAMP}.log +fi + +backup="$(snapctl get backup-on-refresh)" +if [ $backup == "enable" ]; then + backupdb + if ! [ $? == 0 ]; then + echo "Failed rocketchat database backup before refresh" >> $SNAP_COMMON/refresh_${TIMESTAMP}.log + fi +fi + diff --git a/.snapcraft/snap/snapcraft.yaml b/.snapcraft/snap/snapcraft.yaml index ae99be479245e..6796ac5158a31 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -39,6 +39,8 @@ apps: hooks: configure: plugs: [network] + pre-refresh: + plugs: [network] parts: node: plugin: dump @@ -64,6 +66,7 @@ parts: - .node_version.txt - etc - usr + - star.json mongodb: build-packages: - wget diff --git a/.storybook/.babelrc b/.storybook/.babelrc index 3f856fec45fb1..826e22a858c56 100644 --- a/.storybook/.babelrc +++ b/.storybook/.babelrc @@ -6,7 +6,7 @@ "shippedProposals": true, "useBuiltIns": "usage", "corejs": "3", - "modules": "commonjs", + "modules": "commonjs" } ], "@babel/preset-react", diff --git a/HISTORY.md b/HISTORY.md index 25ebcd105aa86..4abcc90030c18 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,162 @@ + +# 3.0.0 +`2020-02-14 · 7 ️️️⚠️ · 10 🎉 · 11 🚀 · 32 🐛 · 43 🔍 · 21 👩‍💻👨‍💻` + +### Engine versions +- Node: `12.14.0` +- NPM: `6.13.4` +- MongoDB: `3.4, 3.6, 4.0` + +### ⚠️ BREAKING CHANGES + +- Filter System messages per room ([#16369](https://github.com/RocketChat/Rocket.Chat/pull/16369)) +- Remove deprecated publications ([#16351](https://github.com/RocketChat/Rocket.Chat/pull/16351)) +- Hide system messages ([#16243](https://github.com/RocketChat/Rocket.Chat/pull/16243)) +- Upgrade to Meteor 1.9 and NodeJS 12 ([#16252](https://github.com/RocketChat/Rocket.Chat/pull/16252)) +- Removed room counter from sidebar ([#16036](https://github.com/RocketChat/Rocket.Chat/pull/16036)) +- Change apps/icon endpoint to return app's icon and use it to show on Ui Kit modal ([#16522](https://github.com/RocketChat/Rocket.Chat/pull/16522)) +- TLS v1.0 and TLS v1.1 were disabled by due to NodeJS update to v12. You can still enable them by using flags like `--tls-min-v1.0` and `--tls-min-v1.1` + +### 🎉 New features + +- Button to download admin server info ([#16059](https://github.com/RocketChat/Rocket.Chat/pull/16059)) +- UiKit - Interactive UI elements for Rocket.Chat Apps ([#16048](https://github.com/RocketChat/Rocket.Chat/pull/16048)) +- Sort the Omnichannel Chat list according to the user preferences ([#16437](https://github.com/RocketChat/Rocket.Chat/pull/16437)) +- Setting to only send plain text emails ([#16065](https://github.com/RocketChat/Rocket.Chat/pull/16065)) +- Check the Omnichannel service status per Department ([#16425](https://github.com/RocketChat/Rocket.Chat/pull/16425)) +- Create a user for the Apps during installation ([#15896](https://github.com/RocketChat/Rocket.Chat/pull/15896) by [@Cool-fire](https://github.com/Cool-fire)) +- Add GUI for customFields in Omnichannel conversations ([#15840](https://github.com/RocketChat/Rocket.Chat/pull/15840) by [@antkaz](https://github.com/antkaz)) +- update on mongo, node and caddy on snap ([#16167](https://github.com/RocketChat/Rocket.Chat/pull/16167)) +- Enforce plain text emails converting from HTML when no text version supplied ([#16063](https://github.com/RocketChat/Rocket.Chat/pull/16063)) +- Setting Top navbar in embedded mode ([#16064](https://github.com/RocketChat/Rocket.Chat/pull/16064)) + +### 🚀 Improvements + +- Request user presence on demand ([#16348](https://github.com/RocketChat/Rocket.Chat/pull/16348)) +- Major overhaul on data importers ([#16279](https://github.com/RocketChat/Rocket.Chat/pull/16279)) +- Changes App user's status when the app was enabled/disabled ([#16392](https://github.com/RocketChat/Rocket.Chat/pull/16392)) +- Log as info level when Method Rate Limiters are reached ([#16446](https://github.com/RocketChat/Rocket.Chat/pull/16446)) +- Show more information related to the Omnichannel room closing data ([#16414](https://github.com/RocketChat/Rocket.Chat/pull/16414)) +- Update katex version ([#16393](https://github.com/RocketChat/Rocket.Chat/pull/16393)) +- Prevent "App user" from being deleted by the admin ([#16373](https://github.com/RocketChat/Rocket.Chat/pull/16373)) +- Improve function to check if setting has changed ([#16181](https://github.com/RocketChat/Rocket.Chat/pull/16181)) +- Status Text form validation ([#16121](https://github.com/RocketChat/Rocket.Chat/pull/16121)) +- Set the color of the cancel button on modals to #bdbebf for enhanced visibiity ([#15913](https://github.com/RocketChat/Rocket.Chat/pull/15913) by [@ritwizsinha](https://github.com/ritwizsinha)) +- Remove NRR ([#16071](https://github.com/RocketChat/Rocket.Chat/pull/16071)) + +### 🐛 Bug fixes + +- Result of get avatar from url can be null ([#16123](https://github.com/RocketChat/Rocket.Chat/pull/16123)) +- Rooms not being marked as read sometimes ([#16397](https://github.com/RocketChat/Rocket.Chat/pull/16397)) +- Container heights ([#16388](https://github.com/RocketChat/Rocket.Chat/pull/16388)) +- Mail Msg Cancel button not closing the flexbar ([#16263](https://github.com/RocketChat/Rocket.Chat/pull/16263) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- Highlight freezing the UI ([#16378](https://github.com/RocketChat/Rocket.Chat/pull/16378)) +- Adding 'lang' tag ([#16375](https://github.com/RocketChat/Rocket.Chat/pull/16375)) +- Role tags missing - Description field explanation ([#16356](https://github.com/RocketChat/Rocket.Chat/pull/16356)) +- The "click to load" text is hard-coded and not translated. ([#16142](https://github.com/RocketChat/Rocket.Chat/pull/16142) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- FileUpload.getBuffer was not working through the Apps-Engine ([#16234](https://github.com/RocketChat/Rocket.Chat/pull/16234)) +- Integrations admin page ([#16183](https://github.com/RocketChat/Rocket.Chat/pull/16183)) +- Readme Help wanted section ([#16197](https://github.com/RocketChat/Rocket.Chat/pull/16197)) +- auto translate cache ([#15768](https://github.com/RocketChat/Rocket.Chat/pull/15768) by [@vickyokrm](https://github.com/vickyokrm)) +- Save password without confirmation ([#16060](https://github.com/RocketChat/Rocket.Chat/pull/16060) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- Break message-attachment text to the next line ([#16039](https://github.com/RocketChat/Rocket.Chat/pull/16039) by [@ritwizsinha](https://github.com/ritwizsinha)) +- SafePorts: Ports 80, 8080 & 443 linked to respective protocols (#16108) ([#16108](https://github.com/RocketChat/Rocket.Chat/pull/16108)) +- Drag and drop disabled when file upload is disabled ([#16049](https://github.com/RocketChat/Rocket.Chat/pull/16049)) +- Video message sent to wrong room ([#16113](https://github.com/RocketChat/Rocket.Chat/pull/16113)) +- "User not found" for direct messages ([#16047](https://github.com/RocketChat/Rocket.Chat/pull/16047)) +- Embedded style when using 'go' command ([#16051](https://github.com/RocketChat/Rocket.Chat/pull/16051)) +- Thread message icon overlapping text ([#16083](https://github.com/RocketChat/Rocket.Chat/pull/16083)) +- Login change language button ([#16085](https://github.com/RocketChat/Rocket.Chat/pull/16085)) +- api-bypass-rate-limiter permission was not working ([#16080](https://github.com/RocketChat/Rocket.Chat/pull/16080)) +- Missing edited icon in newly created messages ([#16484](https://github.com/RocketChat/Rocket.Chat/pull/16484)) +- Read Message after receive a message and the room is opened ([#16473](https://github.com/RocketChat/Rocket.Chat/pull/16473)) +- Send message with pending messages ([#16474](https://github.com/RocketChat/Rocket.Chat/pull/16474)) +- Do not stop on DM imports if one of users was not found ([#16547](https://github.com/RocketChat/Rocket.Chat/pull/16547)) +- Introduce AppLivechatBridge.isOnlineAsync method ([#16467](https://github.com/RocketChat/Rocket.Chat/pull/16467)) +- When copying invite links, multiple toastr messages ([#16578](https://github.com/RocketChat/Rocket.Chat/pull/16578)) +- Livechat Widget version 1.3.1 ([#16580](https://github.com/RocketChat/Rocket.Chat/pull/16580)) +- Error when successfully joining room by invite link ([#16571](https://github.com/RocketChat/Rocket.Chat/pull/16571)) +- Invite links proxy URLs not working when using CDN ([#16581](https://github.com/RocketChat/Rocket.Chat/pull/16581)) +- Bug on starting Jitsi video calls , multiple messages ([#16601](https://github.com/RocketChat/Rocket.Chat/pull/16601)) + +
+🔍 Minor changes + +- Revert importer streamed uploads ([#16465](https://github.com/RocketChat/Rocket.Chat/pull/16465)) +- Regression: Fix app user status change for non-existing user ([#16458](https://github.com/RocketChat/Rocket.Chat/pull/16458)) +- Regression: Fix sending a message not scrolling to bottom ([#16451](https://github.com/RocketChat/Rocket.Chat/pull/16451)) +- LingoHub based on develop ([#16450](https://github.com/RocketChat/Rocket.Chat/pull/16450)) +- Regression: Fix sequential messages grouping ([#16386](https://github.com/RocketChat/Rocket.Chat/pull/16386)) +- Use GitHub Actions to store builds ([#16443](https://github.com/RocketChat/Rocket.Chat/pull/16443)) +- Regression: recent opened rooms being marked as read ([#16442](https://github.com/RocketChat/Rocket.Chat/pull/16442)) +- Regression: Fix status bar margins ([#16438](https://github.com/RocketChat/Rocket.Chat/pull/16438)) +- Update apps engine to 1.12.0-beta.2496 ([#16398](https://github.com/RocketChat/Rocket.Chat/pull/16398)) +- Regression: App deletion wasn’t returning the correct information ([#16360](https://github.com/RocketChat/Rocket.Chat/pull/16360)) +- Lint: Resolve complexity warnings ([#16114](https://github.com/RocketChat/Rocket.Chat/pull/16114)) +- Fix Preview Docker image build ([#16379](https://github.com/RocketChat/Rocket.Chat/pull/16379)) +- Fix assets download on CI ([#16352](https://github.com/RocketChat/Rocket.Chat/pull/16352)) +- Send build artifacts to S3 ([#16237](https://github.com/RocketChat/Rocket.Chat/pull/16237)) +- Add missing translations ([#16150](https://github.com/RocketChat/Rocket.Chat/pull/16150) by [@ritwizsinha](https://github.com/ritwizsinha)) +- Disable PR Docker image build ([#16141](https://github.com/RocketChat/Rocket.Chat/pull/16141)) +- Add Cloud Info to translation dictionary ([#16122](https://github.com/RocketChat/Rocket.Chat/pull/16122) by [@aviral243](https://github.com/aviral243)) +- Merge master into develop & Set version to 2.5.0-develop ([#16107](https://github.com/RocketChat/Rocket.Chat/pull/16107)) +- Fix tests ([#16469](https://github.com/RocketChat/Rocket.Chat/pull/16469)) +- Regression: prevent submit modal ([#16488](https://github.com/RocketChat/Rocket.Chat/pull/16488)) +- Regression: allow private channels to hide system messages ([#16483](https://github.com/RocketChat/Rocket.Chat/pull/16483)) +- Regression: Fix uikit modal closing on click ([#16475](https://github.com/RocketChat/Rocket.Chat/pull/16475)) +- Regression: Fix undefined presence after reconnect ([#16477](https://github.com/RocketChat/Rocket.Chat/pull/16477)) +- Remove users.info being called without need ([#16504](https://github.com/RocketChat/Rocket.Chat/pull/16504)) +- Add Ui Kit container ([#16503](https://github.com/RocketChat/Rocket.Chat/pull/16503)) +- Catch zip errors on import file load ([#16494](https://github.com/RocketChat/Rocket.Chat/pull/16494)) +- Fix: License missing from manual register handler ([#16505](https://github.com/RocketChat/Rocket.Chat/pull/16505)) +- Exclude federated and app users from active user count ([#16489](https://github.com/RocketChat/Rocket.Chat/pull/16489)) +- Regression: Update Uikit ([#16515](https://github.com/RocketChat/Rocket.Chat/pull/16515)) +- Regression: UIKit - Send container info on block actions triggered on a message ([#16514](https://github.com/RocketChat/Rocket.Chat/pull/16514)) +- Use base64 for import files upload to prevent file corruption ([#16516](https://github.com/RocketChat/Rocket.Chat/pull/16516)) +- Regression: Send app info along with interaction payload to the UI ([#16511](https://github.com/RocketChat/Rocket.Chat/pull/16511)) +- Regression: Ui Kit messaging issues (#16513) ([#16513](https://github.com/RocketChat/Rocket.Chat/pull/16513)) +- Regression: update package-lock ([#16528](https://github.com/RocketChat/Rocket.Chat/pull/16528)) +- Regression: UIkit input states ([#16552](https://github.com/RocketChat/Rocket.Chat/pull/16552)) +- Regression: UIKit missing select states: error/disabled ([#16540](https://github.com/RocketChat/Rocket.Chat/pull/16540)) +- Regression: fix read unread messages ([#16562](https://github.com/RocketChat/Rocket.Chat/pull/16562)) +- Regression: UIKit update modal actions ([#16570](https://github.com/RocketChat/Rocket.Chat/pull/16570)) +- Update Apps-Engine version ([#16584](https://github.com/RocketChat/Rocket.Chat/pull/16584)) +- Add breaking notice regarding TLS ([#16575](https://github.com/RocketChat/Rocket.Chat/pull/16575)) +- Regression: Modal onSubmit ([#16556](https://github.com/RocketChat/Rocket.Chat/pull/16556)) +- Regression: send file modal not working via keyboard ([#16607](https://github.com/RocketChat/Rocket.Chat/pull/16607)) +- Fix github actions accessing the github registry ([#16521](https://github.com/RocketChat/Rocket.Chat/pull/16521) by [@mrsimpson](https://github.com/mrsimpson)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Cool-fire](https://github.com/Cool-fire) +- [@antkaz](https://github.com/antkaz) +- [@ashwaniYDV](https://github.com/ashwaniYDV) +- [@aviral243](https://github.com/aviral243) +- [@mrsimpson](https://github.com/mrsimpson) +- [@ritwizsinha](https://github.com/ritwizsinha) +- [@vickyokrm](https://github.com/vickyokrm) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@LuluGO](https://github.com/LuluGO) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@d-gubert](https://github.com/d-gubert) +- [@gabriellsh](https://github.com/gabriellsh) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@lolimay](https://github.com/lolimay) +- [@mariaeduardacunha](https://github.com/mariaeduardacunha) +- [@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) + # 2.4.9 -`2020-02-10 · 1 🐛 · 1 👩‍💻👨‍💻` +`2020-02-10 · 1 🐛 · 1 🔍 · 1 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -10,12 +167,19 @@ - `stdout` streamer infinite loop ([#16452](https://github.com/RocketChat/Rocket.Chat/pull/16452)) +
+🔍 Minor changes + +- Release 2.4.9 ([#16544](https://github.com/RocketChat/Rocket.Chat/pull/16544)) + +
+ ### 👩‍💻👨‍💻 Core Team 🤓 - [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.8 -`2020-02-07 · 1 🔍 · 1 👩‍💻👨‍💻` +`2020-02-07 · 2 🔍 · 1 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -25,6 +189,7 @@
🔍 Minor changes +- Release 2.4.8 ([#16506](https://github.com/RocketChat/Rocket.Chat/pull/16506)) - Update presence package to 2.6.1 ([#16486](https://github.com/RocketChat/Rocket.Chat/pull/16486))
@@ -34,7 +199,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.7 -`2020-02-03 · 1 🐛 · 1 👩‍💻👨‍💻` +`2020-02-03 · 1 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -45,12 +210,20 @@ - Option to make a channel default ([#16433](https://github.com/RocketChat/Rocket.Chat/pull/16433)) +
+🔍 Minor changes + +- Release 2.4.7 ([#16444](https://github.com/RocketChat/Rocket.Chat/pull/16444)) + +
+ ### 👩‍💻👨‍💻 Core Team 🤓 - [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@ggazzo](https://github.com/ggazzo) # 2.4.6 -`2020-01-31 · 2 🔍 · 2 👩‍💻👨‍💻` +`2020-01-31 · 3 🔍 · 3 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -60,6 +233,7 @@
🔍 Minor changes +- Release 2.4.6 ([#16402](https://github.com/RocketChat/Rocket.Chat/pull/16402)) - Revert message properties validation ([#16395](https://github.com/RocketChat/Rocket.Chat/pull/16395)) - Fix index creation for apps_logs collection ([#16401](https://github.com/RocketChat/Rocket.Chat/pull/16401)) @@ -69,6 +243,26 @@ - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 2.4.5 +`2020-01-29 · 1 🔍 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.17.0` +- NPM: `6.13.4` +- MongoDB: `3.4, 3.6, 4.0` + +
+🔍 Minor changes + +- Release 2.4.5 ([#16380](https://github.com/RocketChat/Rocket.Chat/pull/16380)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.4 `2020-01-29 · 1 🐛 · 2 🔍 · 2 👩‍💻👨‍💻` @@ -96,7 +290,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.3 -`2020-01-28 · 2 🐛 · 2 👩‍💻👨‍💻` +`2020-01-28 · 2 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -108,13 +302,20 @@ - Unknown error when sending message if 'Set a User Name to Alias in Message' setting is enabled ([#16347](https://github.com/RocketChat/Rocket.Chat/pull/16347)) - Invite links usage by channel owners/moderators ([#16176](https://github.com/RocketChat/Rocket.Chat/pull/16176)) +
+🔍 Minor changes + +- Release 2.4.3 ([#16358](https://github.com/RocketChat/Rocket.Chat/pull/16358)) + +
+ ### 👩‍💻👨‍💻 Core Team 🤓 - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) - [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.2 -`2020-01-17 · 4 🐛 · 4 👩‍💻👨‍💻` +`2020-01-17 · 4 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -128,6 +329,13 @@ - Integrations list without pagination and outgoing integration creation ([#16233](https://github.com/RocketChat/Rocket.Chat/pull/16233)) - User stuck after reset password ([#16184](https://github.com/RocketChat/Rocket.Chat/pull/16184)) +
+🔍 Minor changes + +- Release 2.4.2 ([#16274](https://github.com/RocketChat/Rocket.Chat/pull/16274)) + +
+ ### 👩‍💻👨‍💻 Core Team 🤓 - [@MarcosSpessatto](https://github.com/MarcosSpessatto) @@ -136,7 +344,7 @@ - [@tassoevan](https://github.com/tassoevan) # 2.4.1 -`2020-01-10 · 2 🐛 · 3 👩‍💻👨‍💻` +`2020-01-10 · 2 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` ### Engine versions - Node: `8.17.0` @@ -148,10 +356,18 @@ - Enable apps change properties of the sender on the message as before ([#16189](https://github.com/RocketChat/Rocket.Chat/pull/16189)) - JS errors on Administration page ([#16139](https://github.com/RocketChat/Rocket.Chat/pull/16139)) +
+🔍 Minor changes + +- Release 2.4.1 ([#16195](https://github.com/RocketChat/Rocket.Chat/pull/16195)) + +
+ ### 👩‍💻👨‍💻 Core Team 🤓 - [@d-gubert](https://github.com/d-gubert) - [@mariaeduardacunha](https://github.com/mariaeduardacunha) +- [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) # 2.4.0 @@ -2665,10 +2881,11 @@ - [@tassoevan](https://github.com/tassoevan) # 0.72.0 -`2018-11-28 · 1 ️️️⚠️ · 6 🎉 · 16 🚀 · 22 🐛 · 79 🔍 · 25 👩‍💻👨‍💻` +`2018-11-28 · 2 ️️️⚠️ · 6 🎉 · 16 🚀 · 22 🐛 · 79 🔍 · 25 👩‍💻👨‍💻` ### ⚠️ BREAKING CHANGES +- Support for Cordova (Rocket.Chat Legacy app) has reached End-of-life, support has been discontinued - Update to Meteor to 1.8 ([#12468](https://github.com/RocketChat/Rocket.Chat/pull/12468)) ### 🎉 New features @@ -6367,4 +6584,4 @@ - [@graywolf336](https://github.com/graywolf336) - [@marceloschmidt](https://github.com/marceloschmidt) - [@rodrigok](https://github.com/rodrigok) -- [@sampaiodiego](https://github.com/sampaiodiego) +- [@sampaiodiego](https://github.com/sampaiodiego) \ No newline at end of file diff --git a/README.md b/README.md index 54ef952d36e19..f09e1b607b4c1 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,7 @@ At Rocket.Chat, our community drives *everything* we do. The Rocket.Chat team is If you are passionate about our project, want to work with a world-leading open source team, and enjoy working remotely at a location of your choice, then we want to talk to you! -Explore current openings below: - -- [Lead Security Researcher and Developer](https://rocketchat.recruitee.com/o/lead-security-researcher-and-developer) - -- [Sales Engineer](https://rocketchat.recruitee.com/o/sales-engineer) - -- [Business Developer/Sales/Channel](https://rocketchat.recruitee.com/o/business-developer-sales-channel) - -- [Front-End Developer](https://rocketchat.recruitee.com/o/frontend-developer) +Get to know more about us and explore current openings at https://jobs.rocket.chat/" # Community Join thousands of members worldwide 24/7 in our [community server](https://open.rocket.chat). diff --git a/app/api/server/api.js b/app/api/server/api.js index dd4125e366432..adda6f7d23ca4 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -179,15 +179,15 @@ export class APIClass extends Restivus { return rateLimiterDictionary[route]; } - shouldVerifyRateLimit(route) { + shouldVerifyRateLimit(route, userId) { return rateLimiterDictionary.hasOwnProperty(route) && settings.get('API_Enable_Rate_Limiter') === true && (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true) - && !(this.userId && hasPermission(this.userId, 'api-bypass-rate-limit')); + && !(userId && hasPermission(userId, 'api-bypass-rate-limit')); } - enforceRateLimit(objectForRateLimitMatch, request, response) { - if (!this.shouldVerifyRateLimit(objectForRateLimitMatch.route)) { + enforceRateLimit(objectForRateLimitMatch, request, response, userId) { + if (!this.shouldVerifyRateLimit(objectForRateLimitMatch.route, userId)) { return; } @@ -321,7 +321,7 @@ export class APIClass extends Restivus { }; try { - api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response); + api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId); if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, options.permissionsRequired))) { throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', { @@ -349,7 +349,7 @@ export class APIClass extends Restivus { 'error-unauthorized': 'unauthorized', }[e.error] || 'failure'; - result = API.v1[apiMethod](e.message, e.error); + result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error); } finally { delete Accounts._accountData[connection.id]; } diff --git a/app/api/server/v1/commands.js b/app/api/server/v1/commands.js index 5657bbb2d7410..3a46a677e4f99 100644 --- a/app/api/server/v1/commands.js +++ b/app/api/server/v1/commands.js @@ -51,7 +51,7 @@ API.v1.addRoute('commands.list', { authRequired: true }, { }, }); -// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value' } +// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value', triggerId: 'value' } API.v1.addRoute('commands.run', { authRequired: true }, { post() { const body = this.bodyParams; @@ -74,7 +74,7 @@ API.v1.addRoute('commands.run', { authRequired: true }, { } const cmd = body.command.toLowerCase(); - if (!slashCommands.commands[body.command.toLowerCase()]) { + if (!slashCommands.commands[cmd]) { return API.v1.failure('The command provided does not exist (or is disabled).'); } @@ -96,7 +96,9 @@ API.v1.addRoute('commands.run', { authRequired: true }, { message.tmid = body.tmid; } - const result = Meteor.runAsUser(user._id, () => slashCommands.run(cmd, params, message)); + const { triggerId } = body; + + const result = Meteor.runAsUser(user._id, () => slashCommands.run(cmd, params, message, triggerId)); return API.v1.success({ result }); }, @@ -137,7 +139,7 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.success({ preview }); }, - // Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif } } + // Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', tmid: 'value', triggerId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif' } } post() { const body = this.bodyParams; const user = this.getLoggedInUser(); @@ -162,6 +164,14 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.failure('The preview item being executed is in the wrong format.'); } + if (body.tmid && typeof body.tmid !== 'string') { + return API.v1.failure('The tmid parameter when provided must be a string.'); + } + + if (body.triggerId && typeof body.triggerId !== 'string') { + return API.v1.failure('The triggerId parameter when provided must be a string.'); + } + const cmd = body.command.toLowerCase(); if (!slashCommands.commands[cmd]) { return API.v1.failure('The command provided does not exist (or is disabled).'); @@ -171,9 +181,24 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { Meteor.call('canAccessRoom', body.roomId, user._id); const params = body.params ? body.params : ''; + const message = { + rid: body.roomId, + }; + + if (body.tmid) { + const thread = Messages.findOneById(body.tmid); + if (!thread || thread.rid !== body.roomId) { + return API.v1.failure('Invalid thread.'); + } + message.tmid = body.tmid; + } Meteor.runAsUser(user._id, () => { - Meteor.call('executeSlashCommandPreview', { cmd, params, msg: { rid: body.roomId } }, body.previewItem); + Meteor.call('executeSlashCommandPreview', { + cmd, + params, + msg: { rid: body.roomId, tmid: body.tmid }, + }, body.previewItem, body.triggerId); }); return API.v1.success(); diff --git a/app/api/server/v1/import.js b/app/api/server/v1/import.js index 0ee09ae5789ca..81f18e38eab73 100644 --- a/app/api/server/v1/import.js +++ b/app/api/server/v1/import.js @@ -1,16 +1,15 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../api'; +import { hasPermission } from '../../../authorization/server'; +import { Imports } from '../../../models/server'; +import { Importers } from '../../../importer/server'; API.v1.addRoute('uploadImportFile', { authRequired: true }, { post() { const { binaryContent, contentType, fileName, importerKey } = this.bodyParams; - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('uploadImportFile', binaryContent, contentType, fileName, importerKey)); - }); - - return API.v1.success(); + return API.v1.success(Meteor.call('uploadImportFile', binaryContent, contentType, fileName, importerKey)); }, }); @@ -28,17 +27,38 @@ API.v1.addRoute('downloadPublicImportFile', { authRequired: true }, { }); +API.v1.addRoute('startImport', { authRequired: true }, { + post() { + const { input } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('startImport', input)); + }); + + return API.v1.success(); + }, +}); + API.v1.addRoute('getImportFileData', { authRequired: true }, { get() { - const { importerKey } = this.requestParams(); let result; Meteor.runAsUser(this.userId, () => { - result = Meteor.call('getImportFileData', importerKey); + result = Meteor.call('getImportFileData'); }); return API.v1.success(result); }, +}); + +API.v1.addRoute('getImportProgress', { authRequired: true }, { + get() { + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('getImportProgress'); + }); + return API.v1.success(result); + }, }); API.v1.addRoute('getLatestImportOperations', { authRequired: true }, { @@ -49,3 +69,46 @@ API.v1.addRoute('getLatestImportOperations', { authRequired: true }, { return API.v1.success(result); }, }); + +API.v1.addRoute('downloadPendingFiles', { authRequired: true }, { + post() { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'downloadPendingFiles' }); + } + + if (!hasPermission(this.userId, 'run-import')) { + throw new Meteor.Error('not_authorized'); + } + + const importer = Importers.get('pending-files'); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', { method: 'downloadPendingFiles' }); + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + const count = importer.instance.prepareFileCount(); + + return API.v1.success({ + success: true, + count, + }); + }, +}); + +API.v1.addRoute('getCurrentImportOperation', { authRequired: true }, { + get() { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getCurrentImportOperation' }); + } + + if (!hasPermission(this.userId, 'run-import')) { + throw new Meteor.Error('not_authorized'); + } + + const operation = Imports.findLastImport(); + return API.v1.success({ + success: true, + operation, + }); + }, +}); diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js index 258cdd9a70c35..59d043e79dd1c 100644 --- a/app/api/server/v1/misc.js +++ b/app/api/server/v1/misc.js @@ -10,7 +10,7 @@ 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/publish'; +import { StdOut } from '../../../logger/server/streamer'; // DEPRECATED diff --git a/app/api/server/v1/stats.js b/app/api/server/v1/stats.js index 2f74a2092947e..bd6d1fa2db6b5 100644 --- a/app/api/server/v1/stats.js +++ b/app/api/server/v1/stats.js @@ -1,48 +1,30 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { Statistics } from '../../../models'; import { API } from '../api'; +import { getStatistics, getLastStatistics } from '../../../statistics/server'; API.v1.addRoute('statistics', { authRequired: true }, { get() { - let refresh = false; - if (typeof this.queryParams.refresh !== 'undefined' && this.queryParams.refresh === 'true') { - refresh = true; - } - - let stats; - Meteor.runAsUser(this.userId, () => { - stats = Meteor.call('getStatistics', refresh); - }); - - return API.v1.success({ - statistics: stats, - }); + const { refresh } = this.requestParams(); + return API.v1.success(Promise.await(getLastStatistics({ + userId: this.userId, + refresh: refresh && refresh === 'true', + }))); }, }); API.v1.addRoute('statistics.list', { authRequired: true }, { get() { - if (!hasPermission(this.userId, 'view-statistics')) { - return API.v1.unauthorized(); - } - const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - const statistics = Statistics.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }).fetch(); - - return API.v1.success({ - statistics, - count: statistics.length, - offset, - total: Statistics.find(query).count(), - }); + return API.v1.success(Promise.await(getStatistics({ + userId: this.userId, + query, + pagination: { + offset, + count, + sort, + fields, + }, + }))); }, }); diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index ca0b0bd161c44..d0115d51fb0c2 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -524,7 +524,6 @@ API.v1.addRoute('users.setPreferences', { authRequired: true }, { hideAvatars: Match.Maybe(Boolean), hideFlexTab: Match.Maybe(Boolean), sendOnEnter: Match.Maybe(String), - roomCounterSidebar: Match.Maybe(Boolean), language: Match.Maybe(String), sidebarShowFavorites: Match.Optional(Boolean), sidebarShowUnread: Match.Optional(Boolean), @@ -648,7 +647,7 @@ API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, { API.v1.addRoute('users.presence', { authRequired: true }, { get() { - const { from } = this.queryParams; + const { from, ids } = this.queryParams; const options = { fields: { @@ -660,6 +659,13 @@ API.v1.addRoute('users.presence', { authRequired: true }, { }, }; + if (ids) { + return API.v1.success({ + users: Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).fetch(), + full: false, + }); + } + if (from) { const ts = new Date(from); const diff = (Date.now() - ts) / 1000 / 60; diff --git a/app/apps/client/admin/appInstall.js b/app/apps/client/admin/appInstall.js index 969486a382202..1db80c13b11e2 100644 --- a/app/apps/client/admin/appInstall.js +++ b/app/apps/client/admin/appInstall.js @@ -11,6 +11,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; import { Tracker } from 'meteor/tracker'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import toastr from 'toastr'; import { APIClient } from '../../../utils'; @@ -19,7 +20,7 @@ import { SideNav } from '../../../ui-utils/client'; function handleInstallError(apiError) { if (!apiError.xhr || !apiError.xhr.responseJSON) { return; } - const { status, messages, error } = apiError.xhr.responseJSON; + const { status, messages, error, payload = null } = apiError.xhr.responseJSON; let message; @@ -27,11 +28,15 @@ function handleInstallError(apiError) { case 'storage_error': message = messages.join(''); break; - case 'compiler_error': message = 'There has been compiler errors. App cannot be installed'; break; - + case 'app_user_error': + message = messages.join(''); + if (payload && payload.username) { + message = TAPi18n.__('Apps_User_Already_Exists', { username: payload.username }); + } + break; default: if (error) { message = error; diff --git a/app/apps/server/bridges/activation.js b/app/apps/server/bridges/activation.js index 30983357bdff3..b926061ed5069 100644 --- a/app/apps/server/bridges/activation.js +++ b/app/apps/server/bridges/activation.js @@ -1,3 +1,7 @@ +import { STATUS_MAP } from '../../../../imports/users-presence/server/activeUsers'; +import { Users } from '../../../models/server'; +import { Notifications } from '../../../notifications/server'; + export class AppActivationBridge { constructor(orch) { this.orch = orch; @@ -16,6 +20,20 @@ export class AppActivationBridge { } async appStatusChanged(app, status) { + const user = Users.findOneByAppId(app.getID(), { fields: { username: 1 } }); + + if (user) { + const { _id, username } = user; + const userStatus = ['auto_enabled', 'manually_enabled'].includes(status) ? 'online' : 'offline'; + + Users.updateStatusById(_id, userStatus); + Notifications.notifyLogged('user-status', [ + _id, + username, + STATUS_MAP[userStatus], + ]); + } + await this.orch.getNotifier().appStatusUpdated(app.getID(), status); } } diff --git a/app/apps/server/bridges/bridges.js b/app/apps/server/bridges/bridges.js index 6298e94980005..c495332e6d1b7 100644 --- a/app/apps/server/bridges/bridges.js +++ b/app/apps/server/bridges/bridges.js @@ -15,6 +15,7 @@ import { AppSettingBridge } from './settings'; import { AppUserBridge } from './users'; import { AppLivechatBridge } from './livechat'; import { AppUploadBridge } from './uploads'; +import { UiInteractionBridge } from './uiInteraction'; export class RealAppBridges extends AppBridges { constructor(orch) { @@ -35,6 +36,7 @@ export class RealAppBridges extends AppBridges { this._userBridge = new AppUserBridge(orch); this._livechatBridge = new AppLivechatBridge(orch); this._uploadBridge = new AppUploadBridge(orch); + this._uiInteractionBridge = new UiInteractionBridge(orch); } getCommandBridge() { @@ -96,4 +98,8 @@ export class RealAppBridges extends AppBridges { getUploadBridge() { return this._uploadBridge; } + + getUiInteractionBridge() { + return this._uiInteractionBridge; + } } diff --git a/app/apps/server/bridges/commands.js b/app/apps/server/bridges/commands.js index aab50a25bc293..87ef7cdd68f07 100644 --- a/app/apps/server/bridges/commands.js +++ b/app/apps/server/bridges/commands.js @@ -91,6 +91,7 @@ export class AppCommandsBridge { this._verifyCommand(command); const item = { + appId, command: command.command.toLowerCase(), params: Utilities.getI18nKeyForApp(command.i18nParamsExample, appId), description: Utilities.getI18nKeyForApp(command.i18nDescription, appId), @@ -145,7 +146,7 @@ export class AppCommandsBridge { } } - _appCommandExecutor(command, parameters, message) { + _appCommandExecutor(command, parameters, message, triggerId) { const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); const room = this.orch.getConverters().get('rooms').convertById(message.rid); const threadId = message.tmid; @@ -156,7 +157,9 @@ export class AppCommandsBridge { Object.freeze(room), Object.freeze(params), threadId, + triggerId, ); + Promise.await(this.orch.getManager().getCommandManager().executeCommand(command, context)); } @@ -175,7 +178,7 @@ export class AppCommandsBridge { return Promise.await(this.orch.getManager().getCommandManager().getPreviews(command, context)); } - _appCommandPreviewExecutor(command, parameters, message, preview) { + _appCommandPreviewExecutor(command, parameters, message, preview, triggerId) { const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); const room = this.orch.getConverters().get('rooms').convertById(message.rid); const threadId = message.tmid; @@ -186,7 +189,9 @@ export class AppCommandsBridge { Object.freeze(room), Object.freeze(params), threadId, + triggerId, ); + Promise.await(this.orch.getManager().getCommandManager().executePreview(command, preview, context)); } } diff --git a/app/apps/server/bridges/listeners.js b/app/apps/server/bridges/listeners.js index 01c039d62e64c..88bdc7c499c47 100644 --- a/app/apps/server/bridges/listeners.js +++ b/app/apps/server/bridges/listeners.js @@ -37,6 +37,17 @@ export class AppListenerBridge { // } } + async uiKitInteractionEvent(inte, action) { + return this.orch.getManager().getListenerManager().executeListener(inte, action); + + // try { + + // } catch (e) { + // this.orch.debugLog(`${ e.name }: ${ e.message }`); + // this.orch.debugLog(e.stack); + // } + } + async livechatEvent(inte, room) { const rm = this.orch.getConverters().get('rooms').convertRoom(room); const result = await this.orch.getManager().getListenerManager().executeListener(inte, rm); diff --git a/app/apps/server/bridges/livechat.js b/app/apps/server/bridges/livechat.js index 5437246a03331..f719b8251cbe1 100644 --- a/app/apps/server/bridges/livechat.js +++ b/app/apps/server/bridges/livechat.js @@ -12,8 +12,12 @@ export class AppLivechatBridge { this.orch = orch; } - isOnline() { - return Livechat.online(); + isOnline(department) { + return Livechat.online(department); + } + + async isOnlineAsync(department) { + return Livechat.online(department); } async createMessage(message, appId) { diff --git a/app/apps/server/bridges/uiInteraction.js b/app/apps/server/bridges/uiInteraction.js new file mode 100644 index 0000000000000..2c726cc2102c2 --- /dev/null +++ b/app/apps/server/bridges/uiInteraction.js @@ -0,0 +1,19 @@ +import { Notifications } from '../../../notifications/server'; + +export class UiInteractionBridge { + constructor(orch) { + this.orch = orch; + } + + async notifyUser(user, interaction, appId) { + this.orch.debugLog(`The App ${ appId } is sending an interaction to user.`); + + const app = this.orch.getManager().getOneById(appId); + + if (!app) { + throw new Error('Invalid app provided'); + } + + Notifications.notifyUser(user.id, 'uiInteraction', interaction); + } +} diff --git a/app/apps/server/bridges/uploads.js b/app/apps/server/bridges/uploads.js index a6a3efc6c1d70..fced605713f93 100644 --- a/app/apps/server/bridges/uploads.js +++ b/app/apps/server/bridges/uploads.js @@ -14,8 +14,10 @@ export class AppUploadBridge { getBuffer(upload, appId) { this.orch.debugLog(`The App ${ appId } is getting the upload: "${ upload.id }"`); + const rocketChatUpload = this.orch.getConverters().get('uploads').convertToRocketChat(upload); + return new Promise((resolve, reject) => { - FileUpload.getBuffer(upload, (error, result) => { + FileUpload.getBuffer(rocketChatUpload, (error, result) => { if (error) { return reject(error); } diff --git a/app/apps/server/bridges/users.js b/app/apps/server/bridges/users.js index 7329777f70268..a35a9502eb636 100644 --- a/app/apps/server/bridges/users.js +++ b/app/apps/server/bridges/users.js @@ -1,4 +1,8 @@ -import { Users } from '../../../models/server'; +import { Random } from 'meteor/random'; + +import { setUserAvatar, checkUsernameAvailability, deleteUser } from '../../../lib/server/functions'; +import { Users } from '../../../models'; + export class AppUserBridge { constructor(orch) { @@ -17,6 +21,64 @@ export class AppUserBridge { return this.orch.getConverters().get('users').convertByUsername(username); } + async getAppUser(appId) { + this.orch.debugLog(`The App ${ appId } is getting its assigned user`); + + const user = Users.findOne({ appId }); + + return this.orch.getConverters().get('users').convertToApp(user); + } + + async create(userDescriptor, appId, { avatarUrl }) { + this.orch.debugLog(`The App ${ appId } is requesting to create a new user.`); + const user = this.orch.getConverters().get('users').convertToRocketChat(userDescriptor); + + if (!user._id) { + user._id = Random.id(); + } + + if (!user.createdAt) { + user.createdAt = new Date(); + } + + switch (user.type) { + case 'app': + if (!checkUsernameAvailability(user.username)) { + throw new Error(`The username "${ user.username }" is already being used. Rename or remove the user using it to install this App`); + } + + Users.insert(user); + + if (avatarUrl) { + setUserAvatar(user, avatarUrl, '', 'local'); + } + + break; + + default: + throw new Error('Creating normal users is currently not supported'); + } + + return user._id; + } + + async remove(user, appId) { + this.orch.debugLog(`The App's user is being removed: ${ appId }`); + + // It's actually not a problem if there is no App user to delete - just means we don't need to do anything more. + if (!user) { + return true; + } + + try { + deleteUser(user.id); + } catch (err) { + throw new Error(`Errors occurred while deleting an app user: ${ err }`); + } + + return true; + } + async getActiveUserCount() { return Users.getActiveLocalUserCount(); } diff --git a/app/apps/server/communication/index.js b/app/apps/server/communication/index.js index 525d6ab56cc2c..251fec9e06e00 100644 --- a/app/apps/server/communication/index.js +++ b/app/apps/server/communication/index.js @@ -1,11 +1,6 @@ import { AppMethods } from './methods'; import { AppsRestApi } from './rest'; -import { AppEvents, AppServerNotifier, AppServerListener } from './websockets'; +import { AppUIKitInteractionApi } from './uikit'; +import { AppEvents, AppServerListener, AppServerNotifier } from './websockets'; -export { - AppMethods, - AppsRestApi, - AppEvents, - AppServerNotifier, - AppServerListener, -}; +export { AppUIKitInteractionApi, AppMethods, AppsRestApi, AppEvents, AppServerNotifier, AppServerListener }; diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 2787a965135a4..ccfd34fdf8486 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -239,6 +239,14 @@ export class AppsRestApi { return API.v1.failure({ status: 'compiler_error', messages: aff.getCompilerErrors() }); } + if (aff.hasAppUserError()) { + return API.v1.failure({ + status: 'app_user_error', + messages: [aff.getAppUserError().message], + payload: { username: aff.getAppUserError().username }, + }); + } + info.status = aff.getApp().getStatus(); return API.v1.success({ @@ -449,15 +457,16 @@ export class AppsRestApi { delete() { const prl = manager.getOneById(this.urlParams.id); - if (prl) { - Promise.await(manager.remove(prl.getID())); + if (!prl) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } - const info = prl.getInfo(); - info.status = prl.getStatus(); + Promise.await(manager.remove(prl.getID())); - return API.v1.success({ app: info }); - } - return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + const info = prl.getInfo(); + info.status = prl.getStatus(); + + return API.v1.success({ app: info }); }, }); @@ -494,16 +503,30 @@ export class AppsRestApi { }, }); - this.api.addRoute(':id/icon', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + this.api.addRoute(':id/icon', { authRequired: false }, { get() { const prl = manager.getOneById(this.urlParams.id); + if (!prl) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } - if (prl) { - const info = prl.getInfo(); - - return API.v1.success({ iconFileContent: info.iconFileContent }); + const info = prl.getInfo(); + if (!info || !info.iconFileContent) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); } - return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + + const imageData = info.iconFileContent.split(';base64,'); + + const buf = Buffer.from(imageData[1], 'base64'); + + return { + statusCode: 200, + headers: { + 'Content-Length': buf.length, + 'Content-Type': imageData[0].replace('data:', ''), + }, + body: buf, + }; }, }); diff --git a/app/apps/server/communication/uikit.js b/app/apps/server/communication/uikit.js new file mode 100644 index 0000000000000..0bad3e7c251f4 --- /dev/null +++ b/app/apps/server/communication/uikit.js @@ -0,0 +1,161 @@ +import express from 'express'; +import { WebApp } from 'meteor/webapp'; +import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; + +import { Users } from '../../../models/server'; + +const apiServer = express(); + +apiServer.disable('x-powered-by'); + +WebApp.connectHandlers.use(apiServer); + +// eslint-disable-next-line new-cap +const router = express.Router(); + +const unauthorized = (res) => + res.status(401).send({ + status: 'error', + message: 'You must be logged in to do this.', + }); + +router.use((req, res, next) => { + const { + 'x-user-id': userId, + 'x-auth-token': authToken, + } = req.headers; + + if (!userId || !authToken) { + return unauthorized(res); + } + + const user = Users.findOneByIdAndLoginToken(userId, authToken); + if (!user) { + return unauthorized(res); + } + + req.user = user; + req.userId = user._id; + + next(); +}); + +apiServer.use('/api/apps/ui.interaction/', router); + +export class AppUIKitInteractionApi { + constructor(orch) { + this.orch = orch; + + router.post('/:appId', (req, res) => { + const { + appId, + } = req.params; + + const { + type, + } = req.body; + + switch (type) { + case UIKitIncomingInteractionType.BLOCK: { + const { + type, + actionId, + triggerId, + mid, + rid, + payload, + container, + } = req.body; + + const room = this.orch.getConverters().get('rooms').convertById(rid); + const user = this.orch.getConverters().get('users').convertToApp(req.user); + const message = mid && this.orch.getConverters().get('messages').convertById(mid); + + const action = { + type, + container, + appId, + actionId, + message, + triggerId, + payload, + user, + room, + }; + + try { + const result = Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e.message); + } + break; + } + + case UIKitIncomingInteractionType.VIEW_CLOSED: { + const { + type, + actionId, + view, + isCleared, + } = req.body; + + const user = this.orch.getConverters().get('users').convertToApp(req.user); + + const action = { + type, + appId, + actionId, + user, + payload: { + view, + isCleared, + }, + }; + + try { + Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); + + res.send(200); + } catch (e) { + console.log(e); + res.status(500).send(e.message); + } + break; + } + + case UIKitIncomingInteractionType.VIEW_SUBMIT: { + const { + type, + actionId, + triggerId, + payload, + } = req.body; + + const user = this.orch.getConverters().get('users').convertToApp(req.user); + + const action = { + type, + appId, + actionId, + triggerId, + payload, + user, + }; + + try { + const result = Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e.message); + } + break; + } + } + + // TODO: validate payloads per type + }); + } +} diff --git a/app/apps/server/converters/messages.js b/app/apps/server/converters/messages.js index 9d7afc4289413..0da7cbd768d20 100644 --- a/app/apps/server/converters/messages.js +++ b/app/apps/server/converters/messages.js @@ -35,6 +35,7 @@ export class AppMessagesConverter { customFields: 'customFields', groupable: 'groupable', token: 'token', + blocks: 'blocks', room: (message) => { const result = this.orch.getConverters().get('rooms').convertById(message.rid); delete message.rid; @@ -135,6 +136,7 @@ export class AppMessagesConverter { attachments, reactions: message.reactions, parseUrls: message.parseUrls, + blocks: message.blocks, token: message.token, }; diff --git a/app/apps/server/converters/users.js b/app/apps/server/converters/users.js index bccbfa0fce53c..79d4016e7c203 100644 --- a/app/apps/server/converters/users.js +++ b/app/apps/server/converters/users.js @@ -41,6 +41,30 @@ export class AppUsersConverter { createdAt: user.createdAt, updatedAt: user._updatedAt, lastLoginAt: user.lastLogin, + appId: user.appId, + }; + } + + convertToRocketChat(user) { + if (!user) { + return undefined; + } + + return { + _id: user.id, + username: user.username, + emails: user.emails, + type: user.type, + active: user.isEnabled, + name: user.name, + roles: user.roles, + status: user.status, + statusConnection: user.statusConnection, + utcOffset: user.utfOffset, + createdAt: user.createdAt, + _updatedAt: user.updatedAt, + lastLogin: user.lastLoginAt, + appId: user.appId, }; } @@ -50,6 +74,8 @@ export class AppUsersConverter { return UserType.USER; case 'bot': return UserType.BOT; + case 'app': + return UserType.APP; case '': case undefined: return UserType.UNKNOWN; diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js index 84c71af4fa0f4..6f2a45bc01baf 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -1,16 +1,17 @@ import { Meteor } from 'meteor/meteor'; import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { Logger } from '../../logger'; +import { AppsLogsModel, AppsModel, AppsPersistenceModel, Permissions } from '../../models'; +import { settings } from '../../settings'; import { RealAppBridges } from './bridges'; -import { AppMethods, AppsRestApi, AppServerNotifier } from './communication'; +import { AppMethods, AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter } from './converters'; -import { AppRealStorage, AppRealLogsStorage } from './storage'; -import { settings } from '../../settings'; -import { Permissions, AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models'; -import { Logger } from '../../logger'; -import { AppVisitorsConverter } from './converters/visitors'; -import { AppUploadsConverter } from './converters/uploads'; import { AppDepartmentsConverter } from './converters/departments'; +import { AppUploadsConverter } from './converters/uploads'; +import { AppVisitorsConverter } from './converters/visitors'; +import { AppRealLogsStorage, AppRealStorage } from './storage'; + class AppServerOrchestrator { constructor() { @@ -46,6 +47,7 @@ class AppServerOrchestrator { this._communicators.set('methods', new AppMethods(this)); this._communicators.set('notifier', new AppServerNotifier(this)); this._communicators.set('restapi', new AppsRestApi(this, this._manager)); + this._communicators.set('uikit', new AppUIKitInteractionApi(this)); this._isInitialized = true; } diff --git a/app/authorization/client/views/permissionsRole.html b/app/authorization/client/views/permissionsRole.html index a8a5ac92c312f..74579e16fe5db 100644 --- a/app/authorization/client/views/permissionsRole.html +++ b/app/authorization/client/views/permissionsRole.html @@ -34,7 +34,9 @@ - +
+

{{_ "Leave the description field blank if you dont want to show the role"}}.

+
{{#if editable}} diff --git a/app/authorization/server/functions/canSendMessage.js b/app/authorization/server/functions/canSendMessage.js index 26099af663f6e..c303154e16b6d 100644 --- a/app/authorization/server/functions/canSendMessage.js +++ b/app/authorization/server/functions/canSendMessage.js @@ -9,10 +9,10 @@ const subscriptionOptions = { }, }; -export const canSendMessageAsync = async (rid, { uid, username }, extraData) => { +export const canSendMessageAsync = async (rid, { uid, username, type }, extraData) => { const room = await Rooms.findOneById(rid); - if (!await canAccessRoomAsync(room, { _id: uid, username }, extraData)) { + if (type !== 'app' && !await canAccessRoomAsync(room, { _id: uid, username }, extraData)) { throw new Error('error-not-allowed'); } @@ -35,4 +35,4 @@ export const canSendMessageAsync = async (rid, { uid, username }, extraData) => return room; }; -export const canSendMessage = (rid, { uid, username }, extraData) => Promise.await(canSendMessageAsync(rid, { uid, username }, extraData)); +export const canSendMessage = (rid, { uid, username, type }, extraData) => Promise.await(canSendMessageAsync(rid, { uid, username, type }, extraData)); diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js index 01ac6e9e80785..80394b15fcfce 100644 --- a/app/authorization/server/index.js +++ b/app/authorization/server/index.js @@ -20,8 +20,7 @@ import './methods/deleteRole'; import './methods/removeRoleFromPermission'; import './methods/removeUserFromRole'; import './methods/saveRole'; -import './publications/permissions'; -import './publications/roles'; +import './streamer/permissions'; import './startup'; export { diff --git a/app/authorization/server/publications/roles.js b/app/authorization/server/publications/roles.js deleted file mode 100644 index c4ca8d926eb52..0000000000000 --- a/app/authorization/server/publications/roles.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Roles } from '../../../models'; -import { clearCache } from '../functions/hasPermission'; - -Meteor.publish('roles', function() { - console.warn('The publication "roles" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - return Roles.find(); -}); - -Roles.on('change', ({ diff }) => { - if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { - // avoid useless changes - return; - } - clearCache(); -}); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 3314bd5869006..6781accad0301 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { Roles, Permissions, Settings } from '../../models'; import { settings } from '../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../lib'; +import { clearCache } from './functions/hasPermission'; Meteor.startup(function() { // Note: @@ -17,15 +18,15 @@ Meteor.startup(function() { { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'add-user-to-any-c-room', roles: ['admin'] }, { _id: 'add-user-to-any-p-room', roles: [] }, - { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot'] }, + { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, { _id: 'archive-room', roles: ['admin', 'owner'] }, { _id: 'assign-admin-role', roles: ['admin'] }, { _id: 'assign-roles', roles: ['admin'] }, { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, { _id: 'bulk-register-user', roles: ['admin'] }, - { _id: 'create-c', roles: ['admin', 'user', 'bot'] }, - { _id: 'create-d', roles: ['admin', 'user', 'bot'] }, - { _id: 'create-p', roles: ['admin', 'user', 'bot'] }, + { _id: 'create-c', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-d', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-p', roles: ['admin', 'user', 'bot', 'app'] }, { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, { _id: 'create-user', roles: ['admin'] }, { _id: 'clean-channel-history', roles: ['admin'] }, @@ -44,9 +45,9 @@ Meteor.startup(function() { { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'edit-room-retention-policy', roles: ['admin'] }, { _id: 'force-delete-message', roles: ['admin', 'owner'] }, - { _id: 'join-without-join-code', roles: ['admin', 'bot'] }, - { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous'] }, - { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] }, + { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, + { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, { _id: 'manage-assets', roles: ['admin'] }, { _id: 'manage-emoji', roles: ['admin'] }, { _id: 'manage-user-status', roles: ['admin'] }, @@ -64,15 +65,15 @@ Meteor.startup(function() { { _id: 'run-migration', roles: ['admin'] }, { _id: 'set-moderator', roles: ['admin', 'owner'] }, { _id: 'set-owner', roles: ['admin', 'owner'] }, - { _id: 'send-many-messages', roles: ['admin', 'bot'] }, + { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, { _id: 'set-leader', roles: ['admin', 'owner'] }, { _id: 'unarchive-room', roles: ['admin'] }, - { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] }, { _id: 'user-generate-access-token', roles: ['admin'] }, - { _id: 'view-d-room', roles: ['admin', 'user', 'bot'] }, + { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app'] }, { _id: 'view-full-other-user-info', roles: ['admin'] }, { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, - { _id: 'view-joined-room', roles: ['guest', 'bot', 'anonymous'] }, + { _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] }, { _id: 'view-join-code', roles: ['admin'] }, { _id: 'view-logs', roles: ['admin'] }, { _id: 'view-other-user-channels', roles: ['admin'] }, @@ -125,6 +126,7 @@ Meteor.startup(function() { { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, { name: 'user', scope: 'Users', description: '' }, { name: 'bot', scope: 'Users', description: '' }, + { name: 'app', scope: 'Users', description: '' }, { name: 'guest', scope: 'Users', description: '' }, { name: 'anonymous', scope: 'Users', description: '' }, { name: 'livechat-agent', scope: 'Users', description: 'Livechat Agent' }, @@ -207,4 +209,12 @@ Meteor.startup(function() { }; settings.onload('*', createPermissionForAddedSetting); + + Roles.on('change', ({ diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { + // avoid useless changes + return; + } + clearCache(); + }); }); diff --git a/app/authorization/server/publications/permissions/emitter.js b/app/authorization/server/streamer/permissions/emitter.js similarity index 100% rename from app/authorization/server/publications/permissions/emitter.js rename to app/authorization/server/streamer/permissions/emitter.js diff --git a/app/authorization/server/publications/permissions/index.js b/app/authorization/server/streamer/permissions/index.js similarity index 100% rename from app/authorization/server/publications/permissions/index.js rename to app/authorization/server/streamer/permissions/index.js diff --git a/app/autotranslate/client/lib/autotranslate.js b/app/autotranslate/client/lib/autotranslate.js index bf3fbd7b64d6a..1ac9e7a6174e6 100644 --- a/app/autotranslate/client/lib/autotranslate.js +++ b/app/autotranslate/client/lib/autotranslate.js @@ -71,7 +71,7 @@ export const AutoTranslate = { Tracker.autorun(() => { Subscriptions.find().observeChanges({ changed: (id, fields) => { - if (fields.hasOwnProperty('autoTranslate')) { + if (fields.hasOwnProperty('autoTranslate') || fields.hasOwnProperty('autoTranslateLanguage')) { mem.clear(this.findSubscriptionByRid); } }, diff --git a/app/autotranslate/client/views/autoTranslateFlexTab.js b/app/autotranslate/client/views/autoTranslateFlexTab.js index 3add779473b39..86d2ec52e3432 100644 --- a/app/autotranslate/client/views/autoTranslateFlexTab.js +++ b/app/autotranslate/client/views/autoTranslateFlexTab.js @@ -99,7 +99,7 @@ Template.autoTranslateFlexTab.onCreated(function() { this.saveSetting = () => { const field = this.editing.get(); const subscription = Subscriptions.findOne({ rid: this.rid, 'u._id': Meteor.userId() }); - const previousLanguage = subscription.autoTranslateLanguage; + const previousLanguage = subscription && subscription.autoTranslateLanguage; let value; switch (field) { case 'autoTranslate': diff --git a/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js index 9b8660572c956..fb8755501854b 100644 --- a/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js +++ b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js @@ -93,9 +93,13 @@ Template.mailMessagesInstructions.helpers({ }); Template.mailMessagesInstructions.events({ - 'click .js-cancel, click .mail-messages__instructions--selected'(e, t) { + 'click .mail-messages__instructions--selected'(e, t) { t.reset(true); }, + 'click .js-cancel'(e, t) { + t.reset(true); + t.data.tabBar.close(); + }, 'click .js-send'(e, instance) { const { selectedUsers, selectedEmails, selectedMessages } = instance; const $emailsInput = instance.$('[name="emails"]'); diff --git a/app/channel-settings/client/index.js b/app/channel-settings/client/index.js index 9834b106c9d90..9f76f0a85cc76 100644 --- a/app/channel-settings/client/index.js +++ b/app/channel-settings/client/index.js @@ -3,5 +3,6 @@ import './startup/tabBar'; import './startup/trackSettingsChange'; import './views/channelSettings.html'; import './views/channelSettings'; +import './views/Multiselect'; export { ChannelSettings } from './lib/ChannelSettings'; diff --git a/app/channel-settings/client/views/Multiselect.html b/app/channel-settings/client/views/Multiselect.html new file mode 100644 index 0000000000000..15e2b1884545a --- /dev/null +++ b/app/channel-settings/client/views/Multiselect.html @@ -0,0 +1,3 @@ + diff --git a/app/channel-settings/client/views/Multiselect.js b/app/channel-settings/client/views/Multiselect.js new file mode 100644 index 0000000000000..160901096f147 --- /dev/null +++ b/app/channel-settings/client/views/Multiselect.js @@ -0,0 +1,23 @@ +import './Multiselect.html'; +import { Template } from 'meteor/templating'; + +import { MultiSelectSettingInput } from '../../../../client/components/admin/settings/inputs/MultiSelectSettingInput'; + + +Template.Multiselect.onRendered(async function() { + const { MeteorProvider } = await import('../../../../client/providers/MeteorProvider'); + const React = await import('react'); + const ReactDOM = await import('react-dom'); + this.container = this.firstNode; + this.autorun(() => { + ReactDOM.render(React.createElement(MeteorProvider, { + children: React.createElement(MultiSelectSettingInput, Template.currentData()), + }), this.container); + }); +}); + + +Template.Multiselect.onDestroyed(async function() { + const ReactDOM = await import('react-dom'); + this.container && ReactDOM.unmountComponentAtNode(this.container); +}); diff --git a/app/channel-settings/client/views/channelSettings.html b/app/channel-settings/client/views/channelSettings.html index 9d6f3b23f757a..67b3fb5ad62e4 100644 --- a/app/channel-settings/client/views/channelSettings.html +++ b/app/channel-settings/client/views/channelSettings.html @@ -145,39 +145,48 @@
{{/if}} {{/with}} - {{#with settings.archived}} - {{#if canView}} -
-
- -
-
- {{/if}} - {{/with}} {{#with settings.sysMes}} {{#if canView}} +
+ {{# if c}} + + {{/if}} +
+ {{/if}} + {{/with}} + + {{#with settings.archived}} + {{#if canView}} +
+
+ +
+
{{/if}} {{/with}} diff --git a/app/channel-settings/client/views/channelSettings.js b/app/channel-settings/client/views/channelSettings.js index 1d56f18ff3c03..5b1a31bfcd560 100644 --- a/app/channel-settings/client/views/channelSettings.js +++ b/app/channel-settings/client/views/channelSettings.js @@ -13,6 +13,8 @@ import { callbacks } from '../../../callbacks'; import { hasPermission, hasAllPermission, hasRole, hasAtLeastOnePermission } from '../../../authorization'; import { t, roomTypes, RoomSettingsEnum } from '../../../utils'; import { ChannelSettings } from '../lib/ChannelSettings'; +import { MessageTypesValues } from '../../../lib/lib/MessageTypes'; + const common = { canLeaveRoom() { @@ -148,6 +150,12 @@ Template.channelSettingsEditing.events({ 'change .js-input-check'(e) { this.value.set(e.currentTarget.checked); }, + 'change .js-input-toggle'(e) { + this.toogle.set(e.currentTarget.checked); + if (!e.currentTarget.checked) { + this.value.set(null); + } + }, 'click .js-reset'(e, t) { const { settings } = t; Object.keys(settings).forEach((key) => settings[key].value.set(settings[key].default.get())); @@ -406,7 +414,7 @@ Template.channelSettingsEditing.onCreated(function() { }, sysMes: { type: 'boolean', - label: 'System_messages', + label: 'Hide_System_Messages', isToggle: true, processing: new ReactiveVar(false), canView() { @@ -415,13 +423,21 @@ Template.channelSettingsEditing.onCreated(function() { RoomSettingsEnum.SYSTEM_MESSAGES, ); }, - getValue() { - return room.sysMes !== false; + onChangeValue() { + return function(value) { this.value.set(value || []); }.bind(this); + }, + getValue(room) { + return room.sysMes; }, canEdit() { return hasAllPermission('edit-room', room._id); }, - save(value) { + get() { + return this.value.get() || []; + }, + save() { + const value = this.toogle.get() ? this.value.get() : null; + return call('saveRoomSettings', room._id, 'systemMessages', value).then( () => { toastr.success( @@ -430,6 +446,13 @@ Template.channelSettingsEditing.onCreated(function() { }, ); }, + c() { + return this.toogle.get(); + }, + toogle: new ReactiveVar(room.sysMes && room.sysMes.length > 0), + values() { + return MessageTypesValues; + }, }, archived: { type: 'boolean', diff --git a/app/channel-settings/server/functions/saveRoomSystemMessages.js b/app/channel-settings/server/functions/saveRoomSystemMessages.js index c1a2ba92f0035..1cc17df4e4659 100644 --- a/app/channel-settings/server/functions/saveRoomSystemMessages.js +++ b/app/channel-settings/server/functions/saveRoomSystemMessages.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; import { Rooms } from '../../../models'; +import { MessageTypesValues } from '../../../lib/lib/MessageTypes'; export const saveRoomSystemMessages = function(rid, systemMessages) { if (!Match.test(rid, String)) { @@ -9,5 +10,10 @@ export const saveRoomSystemMessages = function(rid, systemMessages) { function: 'RocketChat.saveRoomSystemMessages', }); } + if (systemMessages && (!Match.test(systemMessages, [String]) || systemMessages.some((value) => !MessageTypesValues.map(({ key }) => key).includes(value)))) { + throw new Meteor.Error('invalid-room', 'Invalid option', { + function: 'RocketChat.saveRoomSystemMessages', + }); + } return Rooms.setSystemMessagesById(rid, systemMessages); }; diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js index 45738633fd71c..9883340a54513 100644 --- a/app/channel-settings/server/methods/saveRoomSettings.js +++ b/app/channel-settings/server/methods/saveRoomSettings.js @@ -183,7 +183,7 @@ Meteor.methods({ } break; case 'systemMessages': - if (value !== room.sysMes) { + if (JSON.stringify(value) !== JSON.stringify(room.sysMes)) { saveRoomSystemMessages(rid, value, user); } break; diff --git a/app/cloud/server/functions/saveRegistrationData.js b/app/cloud/server/functions/saveRegistrationData.js index 0d238c0a2a5e1..235a2943c5457 100644 --- a/app/cloud/server/functions/saveRegistrationData.js +++ b/app/cloud/server/functions/saveRegistrationData.js @@ -1,4 +1,5 @@ import { Settings } from '../../../models/server/raw'; +import { callbacks } from '../../../callbacks'; export function saveRegistrationData({ workspaceId, @@ -8,6 +9,7 @@ export function saveRegistrationData({ client_secret_expires_at, publicKey, registration_client_uri, + licenseData, }) { return Promise.all([ Settings.updateValueById('Register_Server', true), @@ -18,5 +20,9 @@ export function saveRegistrationData({ Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', client_secret_expires_at), Settings.updateValueById('Cloud_Workspace_PublicKey', publicKey), Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', registration_client_uri), - ]); + Settings.updateValueById('Cloud_Workspace_License', licenseData.license || ''), + ]).then((...results) => { + callbacks.run('workspaceLicenseChanged', licenseData.license); + return results; + }); } diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js index 40745c93fb937..4bebfb030d2fa 100644 --- a/app/custom-oauth/server/custom_oauth_server.js +++ b/app/custom-oauth/server/custom_oauth_server.js @@ -17,6 +17,107 @@ const logger = new Logger('CustomOAuth'); const Services = {}; const BeforeUpdateOrCreateUserFromExternalService = []; +const normalizers = { + // Set 'id' to '_id' for any sources that provide it + _id(identity) { + if (identity._id && !identity.id) { + identity.id = identity._id; + } + }, + + // Fix for Reddit + redit(identity) { + if (identity.result) { + return identity.result; + } + }, + + // Fix WordPress-like identities having 'ID' instead of 'id' + wordpress(identity) { + if (identity.ID && !identity.id) { + identity.id = identity.ID; + } + }, + + // Fix Auth0-like identities having 'user_id' instead of 'id' + user_id(identity) { + if (identity.user_id && !identity.id) { + identity.id = identity.user_id; + } + }, + + characterid(identity) { + if (identity.CharacterID && !identity.id) { + identity.id = identity.CharacterID; + } + }, + + // Fix Dataporten having 'user.userid' instead of 'id' + dataporten(identity) { + if (identity.user && identity.user.userid && !identity.id) { + if (identity.user.userid_sec && identity.user.userid_sec[0]) { + identity.id = identity.user.userid_sec[0]; + } else { + identity.id = identity.user.userid; + } + identity.email = identity.user.email; + } + }, + + // Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id' + xenforo(identity) { + if (identity.user && identity.user.user_id && !identity.id) { + identity.id = identity.user.user_id; + identity.email = identity.user.user_email; + } + }, + + // Fix general 'phid' instead of 'id' from phabricator + phabricator(identity) { + if (identity.phid && !identity.id) { + identity.id = identity.phid; + } + }, + + // Fix Keycloak-like identities having 'sub' instead of 'id' + kaycloak(identity) { + if (identity.sub && !identity.id) { + identity.id = identity.sub; + } + }, + + // Fix OpenShift identities where id is in 'metadata' object + openshift(identity) { + if (!identity.id && identity.metadata && identity.metadata.uid) { + identity.id = identity.metadata.uid; + identity.name = identity.fullName; + } + }, + + // Fix general 'userid' instead of 'id' from provider + userid(identity) { + if (identity.userid && !identity.id) { + identity.id = identity.userid; + } + }, + + // Fix Nextcloud provider + nextcloud(identity) { + if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) { + identity.id = identity.ocs.data.id; + identity.name = identity.ocs.data.displayname; + identity.email = identity.ocs.data.email; + } + }, + + // Fix when authenticating from a meteor app with 'emails' field + meteor(identity) { + if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) { + identity.email = identity.emails[0].address ? identity.emails[0].address : undefined; + } + }, +}; + export class CustomOAuth { constructor(name, options) { logger.debug('Init CustomOAuth', name, options); @@ -221,76 +322,11 @@ export class CustomOAuth { normalizeIdentity(identity) { if (identity) { - // Set 'id' to '_id' for any sources that provide it - if (identity._id && !identity.id) { - identity.id = identity._id; - } - - // Fix for Reddit - if (identity.result) { - identity = identity.result; - } - - // Fix WordPress-like identities having 'ID' instead of 'id' - if (identity.ID && !identity.id) { - identity.id = identity.ID; - } - - // Fix Auth0-like identities having 'user_id' instead of 'id' - if (identity.user_id && !identity.id) { - identity.id = identity.user_id; - } - - if (identity.CharacterID && !identity.id) { - identity.id = identity.CharacterID; - } - - // Fix Dataporten having 'user.userid' instead of 'id' - if (identity.user && identity.user.userid && !identity.id) { - if (identity.user.userid_sec && identity.user.userid_sec[0]) { - identity.id = identity.user.userid_sec[0]; - } else { - identity.id = identity.user.userid; + for (const normalizer of Object.values(normalizers)) { + const result = normalizer(identity); + if (result) { + identity = result; } - identity.email = identity.user.email; - } - - // Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id' - if (identity.user && identity.user.user_id && !identity.id) { - identity.id = identity.user.user_id; - identity.email = identity.user.user_email; - } - // Fix general 'phid' instead of 'id' from phabricator - if (identity.phid && !identity.id) { - identity.id = identity.phid; - } - - // Fix Keycloak-like identities having 'sub' instead of 'id' - if (identity.sub && !identity.id) { - identity.id = identity.sub; - } - - // Fix OpenShift identities where id is in 'metadata' object - if (!identity.id && identity.metadata && identity.metadata.uid) { - identity.id = identity.metadata.uid; - identity.name = identity.fullName; - } - - // Fix general 'userid' instead of 'id' from provider - if (identity.userid && !identity.id) { - identity.id = identity.userid; - } - - // Fix Nextcloud provider - if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) { - identity.id = identity.ocs.data.id; - identity.name = identity.ocs.data.displayname; - identity.email = identity.ocs.data.email; - } - - // Fix when authenticating from a meteor app with 'emails' field - if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) { - identity.email = identity.emails[0].address ? identity.emails[0].address : undefined; } } diff --git a/app/custom-sounds/server/index.js b/app/custom-sounds/server/index.js index 873a373e9191f..d6b84fae0269c 100644 --- a/app/custom-sounds/server/index.js +++ b/app/custom-sounds/server/index.js @@ -5,4 +5,3 @@ import './methods/deleteCustomSound'; import './methods/insertOrUpdateSound'; import './methods/listCustomSounds'; import './methods/uploadCustomSound'; -import './publications/customSounds'; diff --git a/app/custom-sounds/server/publications/customSounds.js b/app/custom-sounds/server/publications/customSounds.js deleted file mode 100644 index d088cc78efcd6..0000000000000 --- a/app/custom-sounds/server/publications/customSounds.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import s from 'underscore.string'; - -import { CustomSounds } from '../../../models'; - -Meteor.publish('customSounds', function(filter, limit) { - console.warn('The publication "customSounds" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - const fields = { - name: 1, - extension: 1, - }; - - filter = s.trim(filter); - - const options = { - fields, - limit, - sort: { name: 1 }, - }; - - if (filter) { - const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); - return CustomSounds.findByName(filterReg, options); - } - - return CustomSounds.find({}, options); -}); diff --git a/app/discussion/client/views/DiscussionTabbar.html b/app/discussion/client/views/DiscussionTabbar.html index 16e5bd0488c30..b489dcedb79f3 100644 --- a/app/discussion/client/views/DiscussionTabbar.html +++ b/app/discussion/client/views/DiscussionTabbar.html @@ -10,7 +10,7 @@

{{_ "No_discussions_yet"}}

diff --git a/app/discussion/client/views/DiscussionTabbar.js b/app/discussion/client/views/DiscussionTabbar.js index 4fe0175898cf6..956428e899f10 100644 --- a/app/discussion/client/views/DiscussionTabbar.js +++ b/app/discussion/client/views/DiscussionTabbar.js @@ -20,9 +20,6 @@ Template.discussionsTabbar.helpers({ const instance = Template.instance(); return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, - message() { - return _.extend(this, { customClass: 'pinned', actionContext: 'pinned' }); - }, hasMore() { return Template.instance().hasMore.get(); }, diff --git a/app/discussion/server/index.js b/app/discussion/server/index.js index 5e8fadefa9438..32b341422bf65 100644 --- a/app/discussion/server/index.js +++ b/app/discussion/server/index.js @@ -3,8 +3,6 @@ import './authorization'; import './permissions'; import './hooks/propagateDiscussionMetadata'; -import './publications/discussionParentAutocomplete'; -import './publications/discussionsOfRoom'; // Methods import './methods/createDiscussion'; diff --git a/app/discussion/server/publications/discussionParentAutocomplete.js b/app/discussion/server/publications/discussionParentAutocomplete.js deleted file mode 100644 index dbe50fbe4208e..0000000000000 --- a/app/discussion/server/publications/discussionParentAutocomplete.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Rooms } from '../../../models/server'; -import { hasPermission } from '../../../authorization/server'; - -Meteor.publish('discussionParentAutocomplete', function(selector) { - console.warn('The publication "discussionParentAutocomplete" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - if (hasPermission(this.userId, 'view-c-room') !== true) { - return this.ready(); - } - - const pub = this; - const options = { - fields: { - _id: 1, - name: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const cursorHandle = Rooms.findDiscussionParentByNameStarting(selector.name, options).observeChanges({ - added(_id, record) { - return pub.added('autocompleteRecords', _id, record); - }, - changed(_id, record) { - return pub.changed('autocompleteRecords', _id, record); - }, - removed(_id, record) { - return pub.removed('autocompleteRecords', _id, record); - }, - }); - - this.ready(); - - this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/discussion/server/publications/discussionsOfRoom.js b/app/discussion/server/publications/discussionsOfRoom.js deleted file mode 100644 index 23f70605b53a9..0000000000000 --- a/app/discussion/server/publications/discussionsOfRoom.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Messages } from '../../../models/server'; - -Meteor.publish('discussionsOfRoom', function(rid, limit = 50) { - console.warn('The publication "discussionsOfRoom" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - const publication = this; - - if (!Meteor.call('canAccessRoom', rid, this.userId)) { - return this.ready(); - } - - const cursorHandle = Messages.find({ rid, drid: { $exists: true } }, { sort: { ts: -1 }, limit }).observeChanges({ - added(_id, record) { - return publication.added('rocketchat_discussions_of_room', _id, record); - }, - changed(_id, record) { - return publication.changed('rocketchat_discussions_of_room', _id, record); - }, - removed(_id) { - return publication.removed('rocketchat_discussions_of_room', _id); - }, - }); - - this.ready(); - return this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/emoji-custom/server/index.js b/app/emoji-custom/server/index.js index 162ae9a35d141..880398b6c1e2f 100644 --- a/app/emoji-custom/server/index.js +++ b/app/emoji-custom/server/index.js @@ -1,6 +1,5 @@ import './startup/emoji-custom'; import './startup/settings'; -import './publications/fullEmojiData'; import './methods/listEmojiCustom'; import './methods/deleteEmojiCustom'; import './methods/insertOrUpdateEmoji'; diff --git a/app/emoji-custom/server/publications/fullEmojiData.js b/app/emoji-custom/server/publications/fullEmojiData.js deleted file mode 100644 index f0e0d91c0ad53..0000000000000 --- a/app/emoji-custom/server/publications/fullEmojiData.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import s from 'underscore.string'; - -import { EmojiCustom } from '../../../models'; - -Meteor.publish('fullEmojiData', function(filter, limit) { - console.warn('The publication "fullEmojiData" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - const fields = { - name: 1, - aliases: 1, - extension: 1, - }; - - filter = s.trim(filter); - - const options = { - fields, - limit, - sort: { name: 1 }, - }; - - if (filter) { - const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); - return EmojiCustom.findByNameOrAlias(filterReg, options); - } - - return EmojiCustom.find({}, options); -}); diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js index 0dae5285f9d22..6d776db215978 100644 --- a/app/federation/server/endpoints/dispatch.js +++ b/app/federation/server/endpoints/dispatch.js @@ -22,388 +22,403 @@ import { getUpload, requestEventsFromLatest } from '../handler'; import { notifyUsersOnMessage } from '../../../lib/server/lib/notifyUsersOnMessage'; import { sendAllNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; -API.v1.addRoute('federation.events.dispatch', { authRequired: false }, { - async post() { - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); +const eventHandlers = { + // + // PING + // + async [eventTypes.PING]() { + return { + success: true, + }; + }, + + // + // GENESIS + // + async [eventTypes.GENESIS](event) { + switch (event.data.contextType) { + case contextDefinitions.ROOM.type: + const eventResult = await FederationRoomEvents.addEvent(event.context, event); + + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { room } } = event; + + // Check if room exists + const persistedRoom = Rooms.findOne({ _id: room._id }); + + if (persistedRoom) { + // Update the federation + Rooms.update({ _id: persistedRoom._id }, { $set: { federation: room.federation } }); + } else { + // Denormalize room + const denormalizedRoom = normalizers.denormalizeRoom(room); + + // Create the room + Rooms.insert(denormalizedRoom); + } + } + return eventResult; } + }, - // - // Decrypt the payload if needed - let payload; + // + // ROOM_DELETE + // + async [eventTypes.ROOM_DELETE](event) { + const { data: { roomId } } = event; - try { - payload = decryptIfNeeded(this.request, this.bodyParams); - } catch (err) { - return API.v1.failure('Could not decrypt payload'); + // Check if room exists + const persistedRoom = Rooms.findOne({ _id: roomId }); + + if (persistedRoom) { + // Delete the room + deleteRoom(roomId); } - // - // Convert from EJSON - const { events } = EJSON.fromJSONValue(payload); + // Remove all room events + await FederationRoomEvents.removeRoomEvents(roomId); - logger.server.debug(`federation.events.dispatch => events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + return { + success: true, + }; + }, - // Loop over received events - for (const event of events) { - /* eslint-disable no-await-in-loop */ + // + // ROOM_ADD_USER + // + async [eventTypes.ROOM_ADD_USER0](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - let eventResult; + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { roomId, user, subscription, domainsAfterAdd } } = event; - switch (event.type) { - // - // PING - // - case eventTypes.PING: - eventResult = { - success: true, - }; - break; - - // - // GENESIS - // - case eventTypes.GENESIS: - switch (event.data.contextType) { - case contextDefinitions.ROOM.type: - eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { room } } = event; - - // Check if room exists - const persistedRoom = Rooms.findOne({ _id: room._id }); - - if (persistedRoom) { - // Update the federation - Rooms.update({ _id: persistedRoom._id }, { $set: { federation: room.federation } }); - } else { - // Denormalize room - const denormalizedRoom = normalizers.denormalizeRoom(room); - - // Create the room - Rooms.insert(denormalizedRoom); - } - } - break; - } - break; + // Check if user exists + const persistedUser = Users.findOne({ _id: user._id }); - // - // ROOM_DELETE - // - case eventTypes.ROOM_DELETE: - const { data: { roomId } } = event; + if (persistedUser) { + // Update the federation + Users.update({ _id: persistedUser._id }, { $set: { federation: user.federation } }); + } else { + // Denormalize user + const denormalizedUser = normalizers.denormalizeUser(user); - // Check if room exists - const persistedRoom = Rooms.findOne({ _id: roomId }); + // Create the user + Users.insert(denormalizedUser); + } - if (persistedRoom) { - // Delete the room - deleteRoom(roomId); - } + // Check if subscription exists + const persistedSubscription = Subscriptions.findOne({ _id: subscription._id }); - // Remove all room events - await FederationRoomEvents.removeRoomEvents(roomId); + if (persistedSubscription) { + // Update the federation + Subscriptions.update({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } }); + } else { + // Denormalize subscription + const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); - eventResult = { - success: true, - }; + // Create the subscription + Subscriptions.insert(denormalizedSubscription); + } - break; + // Refresh the servers list + FederationServers.refreshServers(); - // - // ROOM_ADD_USER - // - case eventTypes.ROOM_ADD_USER: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // Update the room's federation property + Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); + } - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { roomId, user, subscription, domainsAfterAdd } } = event; + return eventResult; + }, + + // + // ROOM_REMOVE_USER + // + async [eventTypes.ROOM_REMOVE_USER](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); + + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { roomId, user, domainsAfterRemoval } } = event; - // Check if user exists - const persistedUser = Users.findOne({ _id: user._id }); + // Remove the user's subscription + Subscriptions.removeByRoomIdAndUserId(roomId, user._id); - if (persistedUser) { - // Update the federation - Users.update({ _id: persistedUser._id }, { $set: { federation: user.federation } }); - } else { - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); + // Refresh the servers list + FederationServers.refreshServers(); - // Create the user - Users.insert(denormalizedUser); - } + // Update the room's federation property + Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); + } - // Check if subscription exists - const persistedSubscription = Subscriptions.findOne({ _id: subscription._id }); + return eventResult; + }, - if (persistedSubscription) { - // Update the federation - Subscriptions.update({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } }); - } else { - // Denormalize subscription - const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); + // + // ROOM_MESSAGE + // + async [eventTypes.ROOM_MESSAGE](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - // Create the subscription - Subscriptions.insert(denormalizedSubscription); - } + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { message } } = event; - // Refresh the servers list - FederationServers.refreshServers(); + // Check if message exists + const persistedMessage = Messages.findOne({ _id: message._id }); - // Update the room's federation property - Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); - } - break; + if (persistedMessage) { + // Update the federation + Messages.update({ _id: persistedMessage._id }, { $set: { federation: message.federation } }); + } else { + // Load the room + const room = Rooms.findOneById(message.rid); - // - // ROOM_REMOVE_USER - // - case eventTypes.ROOM_REMOVE_USER: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // Denormalize message + const denormalizedMessage = normalizers.denormalizeMessage(message); - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { roomId, user, domainsAfterRemoval } } = event; + // Is there a file? + if (denormalizedMessage.file) { + const fileStore = FileUpload.getStore('Uploads'); - // Remove the user's subscription - Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + const { federation: { origin } } = denormalizedMessage; - // Refresh the servers list - FederationServers.refreshServers(); + const { upload, buffer } = getUpload(origin, denormalizedMessage.file._id); - // Update the room's federation property - Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); + const oldUploadId = upload._id; + + // Normalize upload + delete upload._id; + upload.rid = denormalizedMessage.rid; + upload.userId = denormalizedMessage.u._id; + upload.federation = { + _id: denormalizedMessage.file._id, + origin, + }; + + Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); + + // Update the message's file + denormalizedMessage.file._id = upload._id; + + // Update the message's attachments + for (const attachment of denormalizedMessage.attachments) { + attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); + attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); } - break; + } + + // Create the message + Messages.insert(denormalizedMessage); - // - // ROOM_MESSAGE - // - case eventTypes.ROOM_MESSAGE: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // Notify users + notifyUsersOnMessage(denormalizedMessage, room); + sendAllNotifications(denormalizedMessage, room); + } + } - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { message } } = event; + return eventResult; + }, - // Check if message exists - const persistedMessage = Messages.findOne({ _id: message._id }); + // + // ROOM_EDIT_MESSAGE + // + async [eventTypes.ROOM_EDIT_MESSAGE](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); + + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { message } } = event; + + // Check if message exists + const persistedMessage = Messages.findOne({ _id: message._id }); + + if (!persistedMessage) { + eventResult.success = false; + eventResult.reason = 'missingMessageToEdit'; + } else { + // Update the message + Messages.update({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } }); + } + } - if (persistedMessage) { - // Update the federation - Messages.update({ _id: persistedMessage._id }, { $set: { federation: message.federation } }); - } else { - // Load the room - const room = Rooms.findOneById(message.rid); + return eventResult; + }, - // Denormalize message - const denormalizedMessage = normalizers.denormalizeMessage(message); + // + // ROOM_DELETE_MESSAGE + // + async [eventTypes.ROOM_DELETE_MESSAGE](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - // Is there a file? - if (denormalizedMessage.file) { - const fileStore = FileUpload.getStore('Uploads'); + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { roomId, messageId } } = event; - const { federation: { origin } } = denormalizedMessage; + // Remove the message + Messages.removeById(messageId); - const { upload, buffer } = getUpload(origin, denormalizedMessage.file._id); + // Notify the room + Notifications.notifyRoom(roomId, 'deleteMessage', { _id: messageId }); + } - const oldUploadId = upload._id; + return eventResult; + }, - // Normalize upload - delete upload._id; - upload.rid = denormalizedMessage.rid; - upload.userId = denormalizedMessage.u._id; - upload.federation = { - _id: denormalizedMessage.file._id, - origin, - }; + // + // ROOM_SET_MESSAGE_REACTION + // + async [eventTypes.ROOM_SET_MESSAGE_REACTION](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); + + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { messageId, username, reaction } } = event; + + // Get persisted message + const persistedMessage = Messages.findOne({ _id: messageId }); + + // Make sure reactions exist + persistedMessage.reactions = persistedMessage.reactions || {}; + + let reactionObj = persistedMessage.reactions[reaction]; + + // If there are no reactions of that type, add it + if (!reactionObj) { + reactionObj = { + usernames: [username], + }; + } else { + // Otherwise, add the username + reactionObj.usernames.push(username); + reactionObj.usernames = [...new Set(reactionObj.usernames)]; + } - Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); + // Update the property + Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); + } - // Update the message's file - denormalizedMessage.file._id = upload._id; + return eventResult; + }, - // Update the message's attachments - for (const attachment of denormalizedMessage.attachments) { - attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); - attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); - } - } + // + // ROOM_UNSET_MESSAGE_REACTION + // + async [eventTypes.ROOM_UNSET_MESSAGE_REACTION](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - // Create the message - Messages.insert(denormalizedMessage); + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { messageId, username, reaction } } = event; - // Notify users - notifyUsersOnMessage(denormalizedMessage, room); - sendAllNotifications(denormalizedMessage, room); - } - } - break; - - // - // ROOM_EDIT_MESSAGE - // - case eventTypes.ROOM_EDIT_MESSAGE: - eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { message } } = event; - - // Check if message exists - const persistedMessage = Messages.findOne({ _id: message._id }); - - if (!persistedMessage) { - eventResult.success = false; - eventResult.reason = 'missingMessageToEdit'; - } else { - // Update the message - Messages.update({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } }); - } - } - break; + // Get persisted message + const persistedMessage = Messages.findOne({ _id: messageId }); - // - // ROOM_DELETE_MESSAGE - // - case eventTypes.ROOM_DELETE_MESSAGE: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // Make sure reactions exist + persistedMessage.reactions = persistedMessage.reactions || {}; - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { roomId, messageId } } = event; + // If there are no reactions of that type, ignore + if (!persistedMessage.reactions[reaction]) { + return eventResult; + } - // Remove the message - Messages.removeById(messageId); + const reactionObj = persistedMessage.reactions[reaction]; - // Notify the room - Notifications.notifyRoom(roomId, 'deleteMessage', { _id: messageId }); - } - break; - - // - // ROOM_SET_MESSAGE_REACTION - // - case eventTypes.ROOM_SET_MESSAGE_REACTION: - eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { messageId, username, reaction } } = event; - - // Get persisted message - const persistedMessage = Messages.findOne({ _id: messageId }); - - // Make sure reactions exist - persistedMessage.reactions = persistedMessage.reactions || {}; - - let reactionObj = persistedMessage.reactions[reaction]; - - // If there are no reactions of that type, add it - if (!reactionObj) { - reactionObj = { - usernames: [username], - }; - } else { - // Otherwise, add the username - reactionObj.usernames.push(username); - reactionObj.usernames = [...new Set(reactionObj.usernames)]; - } - - // Update the property - Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); - } - break; + // Get the username index on the list + const usernameIdx = reactionObj.usernames.indexOf(username); - // - // ROOM_UNSET_MESSAGE_REACTION - // - case eventTypes.ROOM_UNSET_MESSAGE_REACTION: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // If the index is not found, ignore + if (usernameIdx === -1) { + return eventResult; + } - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { messageId, username, reaction } } = event; + // Remove the username from the given reaction + reactionObj.usernames.splice(usernameIdx, 1); - // Get persisted message - const persistedMessage = Messages.findOne({ _id: messageId }); + // If there are no more users for that reaction, remove the property + if (reactionObj.usernames.length === 0) { + Messages.update({ _id: messageId }, { $unset: { [`reactions.${ reaction }`]: 1 } }); + } else { + // Otherwise, update the property + Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); + } + } - // Make sure reactions exist - persistedMessage.reactions = persistedMessage.reactions || {}; + return eventResult; + }, - // If there are no reactions of that type, ignore - if (!persistedMessage.reactions[reaction]) { - continue; - } + // + // ROOM_MUTE_USER + // + async [eventTypes.ROOM_MUTE_USER](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - const reactionObj = persistedMessage.reactions[reaction]; + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { roomId, user } } = event; - // Get the username index on the list - const usernameIdx = reactionObj.usernames.indexOf(username); + // Denormalize user + const denormalizedUser = normalizers.denormalizeUser(user); - // If the index is not found, ignore - if (usernameIdx === -1) { - continue; - } + // Mute user + Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username); + } - // Remove the username from the given reaction - reactionObj.usernames.splice(usernameIdx, 1); + return eventResult; + }, - // If there are no more users for that reaction, remove the property - if (reactionObj.usernames.length === 0) { - Messages.update({ _id: messageId }, { $unset: { [`reactions.${ reaction }`]: 1 } }); - } else { - // Otherwise, update the property - Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); - } - } - break; + // + // ROOM_UNMUTE_USER + // + async [eventTypes.ROOM_UNMUTE_USER](event) { + const eventResult = await FederationRoomEvents.addEvent(event.context, event); - // - // ROOM_MUTE_USER - // - case eventTypes.ROOM_MUTE_USER: - eventResult = await FederationRoomEvents.addEvent(event.context, event); + // If the event was successfully added, handle the event locally + if (eventResult.success) { + const { data: { roomId, user } } = event; - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { roomId, user } } = event; + // Denormalize user + const denormalizedUser = normalizers.denormalizeUser(user); - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); + // Mute user + Rooms.unmuteUsernameByRoomId(roomId, denormalizedUser.username); + } - // Mute user - Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username); - } - break; + return eventResult; + }, +}; - // - // ROOM_UNMUTE_USER - // - case eventTypes.ROOM_UNMUTE_USER: - eventResult = await FederationRoomEvents.addEvent(event.context, event); +API.v1.addRoute('federation.events.dispatch', { authRequired: false }, { + async post() { + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); + } - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { data: { roomId, user } } = event; + // + // Decrypt the payload if needed + let payload; - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); + try { + payload = decryptIfNeeded(this.request, this.bodyParams); + } catch (err) { + return API.v1.failure('Could not decrypt payload'); + } - // Mute user - Rooms.unmuteUsernameByRoomId(roomId, denormalizedUser.username); - } - break; + // + // Convert from EJSON + const { events } = EJSON.fromJSONValue(payload); + + logger.server.debug(`federation.events.dispatch => events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + + // Loop over received events + for (const event of events) { + /* eslint-disable no-await-in-loop */ + + let eventResult; - // - // Could not find event - // - default: - continue; + if (eventHandlers[event.type]) { + eventResult = await eventHandlers[event.type](event); } // If there was an error handling the event, take action diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index 46f670fb6adbb..b5a9c9d36c46e 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -494,15 +494,7 @@ export class FileUploadClass { return store.delete(file._id); } - insert(fileData, streamOrBuffer, cb) { - fileData.size = parseInt(fileData.size) || 0; - - // Check if the fileData matches store filter - const filter = this.store.getFilter(); - if (filter && filter.check) { - filter.check(fileData); - } - + _doInsert(fileData, streamOrBuffer, cb) { const fileId = this.store.create(fileData); const token = this.store.createToken(fileId); const tmpFile = UploadFS.getTempFilePath(fileId); @@ -531,4 +523,16 @@ export class FileUploadClass { } } } + + insert(fileData, streamOrBuffer, cb) { + fileData.size = parseInt(fileData.size) || 0; + + // Check if the fileData matches store filter + const filter = this.store.getFilter(); + if (filter && filter.check) { + filter.check(fileData); + } + + return this._doInsert(fileData, streamOrBuffer, cb); + } } diff --git a/app/importer-csv/server/importer.js b/app/importer-csv/server/importer.js index e87da56b8f159..bac5254a8773c 100644 --- a/app/importer-csv/server/importer.js +++ b/app/importer-csv/server/importer.js @@ -1,50 +1,69 @@ import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; import { Accounts } from 'meteor/accounts-base'; import { + RawImports, Base, ProgressStep, Selection, SelectionChannel, SelectionUser, + ImporterWebsocket, } from '../../importer/server'; -import { RocketChatFile } from '../../file'; import { Users, Rooms } from '../../models'; -import { sendMessage } from '../../lib'; +import { insertMessage } from '../../lib'; import { t } from '../../utils'; export class CsvImporter extends Base { - constructor(info) { - super(info); + constructor(info, importRecord) { + super(info, importRecord); this.csvParser = require('csv-parse/lib/sync'); - this.messages = new Map(); } - prepare(dataURI, sentContentType, fileName) { - super.prepare(dataURI, sentContentType, fileName); + prepareUsingLocalFile(fullFilePath) { + this.logger.debug('start preparing import operation'); + this.collection.remove({}); - const uriResult = RocketChatFile.dataURIParse(dataURI); - const zip = new this.AdmZip(new Buffer(uriResult.image, 'base64')); - const zipEntries = zip.getEntries(); + const zip = new this.AdmZip(fullFilePath); + const totalEntries = zip.getEntryCount(); + + ImporterWebsocket.progressUpdated({ rate: 0 }); let tempChannels = []; let tempUsers = []; let hasDirectMessages = false; - const tempMessages = new Map(); - for (const entry of zipEntries) { + let count = 0; + let oldRate = 0; + + const increaseCount = () => { + try { + count++; + const rate = Math.floor(count * 1000 / totalEntries) / 10; + if (rate > oldRate) { + ImporterWebsocket.progressUpdated({ rate }); + oldRate = rate; + } + } catch (e) { + console.error(e); + } + }; + + let messagesCount = 0; + zip.forEach((entry) => { this.logger.debug(`Entry: ${ entry.entryName }`); // Ignore anything that has `__MACOSX` in it's name, as sadly these things seem to mess everything up if (entry.entryName.indexOf('__MACOSX') > -1) { this.logger.debug(`Ignoring the file: ${ entry.entryName }`); - continue; + return increaseCount(); } // Directories are ignored, since they are "virtual" in a zip file if (entry.isDirectory) { this.logger.debug(`Ignoring the directory entry: ${ entry.entryName }`); - continue; + return increaseCount(); } // Parse the channels @@ -52,60 +71,78 @@ export class CsvImporter extends Base { super.updateProgress(ProgressStep.PREPARING_CHANNELS); const parsedChannels = this.csvParser(entry.getData().toString()); tempChannels = parsedChannels.map((c) => ({ - id: c[0].trim().replace('.', '_'), + id: Random.id(), name: c[0].trim(), creator: c[1].trim(), isPrivate: c[2].trim().toLowerCase() === 'private', members: c[3].trim().split(';').map((m) => m.trim()), })); - continue; + return increaseCount(); } // Parse the users if (entry.entryName.toLowerCase() === 'users.csv') { super.updateProgress(ProgressStep.PREPARING_USERS); const parsedUsers = this.csvParser(entry.getData().toString()); - tempUsers = parsedUsers.map((u) => ({ id: u[0].trim().replace('.', '_'), username: u[0].trim(), email: u[1].trim(), name: u[2].trim() })); - continue; + tempUsers = parsedUsers.map((u) => ({ id: Random.id(), username: u[0].trim(), email: u[1].trim(), name: u[2].trim() })); + + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers }); + super.updateRecord({ 'count.users': tempUsers.length }); + super.addCountToTotal(tempUsers.length); + + return increaseCount(); } // Parse the messages if (entry.entryName.indexOf('/') > -1) { - const item = entry.entryName.split('/'); // random/messages.csv - const channelName = item[0]; // random - - if (!tempMessages.get(channelName)) { - tempMessages.set(channelName, new Map()); + if (this.progress.step !== ProgressStep.PREPARING_MESSAGES) { + super.updateProgress(ProgressStep.PREPARING_MESSAGES); } + const item = entry.entryName.split('/'); // random/messages.csv + const folderName = item[0]; // random + let msgs = []; try { msgs = this.csvParser(entry.getData().toString()); } catch (e) { this.logger.warn(`The file ${ entry.entryName } contains invalid syntax`, e); - continue; + return increaseCount(); } - if (channelName.toLowerCase() === 'directmessages') { + let data; + const msgGroupData = item[1].split('.')[0]; // messages + + if (folderName.toLowerCase() === 'directmessages') { hasDirectMessages = true; - const fileName = item[1]; - const data = msgs.map((m) => ({ username: m[0], ts: m[2], text: m[3], otherUsername: m[1], isDirect: true })); + data = msgs.map((m) => ({ username: m[0], ts: m[2], text: m[3], otherUsername: m[1], isDirect: true })); + } else { + data = msgs.map((m) => ({ username: m[0], ts: m[1], text: m[2] })); + } + + messagesCount += data.length; + const channelName = `${ folderName }/${ msgGroupData }`; + + super.updateRecord({ messagesstatus: channelName }); - tempMessages.get(channelName).set(fileName, data); + if (Base.getBSONSize(data) > Base.getMaxBSONSize()) { + Base.getBSONSafeArraysFromAnArray(data).forEach((splitMsg, i) => { + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channelName }.${ i }`, messages: splitMsg, channel: folderName, i, msgGroupData }); + }); } else { - const msgGroupData = item[1].split('.')[0]; // messages - tempMessages.get(channelName).set(msgGroupData, msgs.map((m) => ({ username: m[0], ts: m[1], text: m[2] }))); + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: channelName, messages: data, channel: folderName, msgGroupData }); } + + super.updateRecord({ 'count.messages': messagesCount, messagesstatus: null }); + return increaseCount(); } - } - // Insert the users record, eventually this might have to be split into several ones as well - // if someone tries to import a several thousands users instance - const usersId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers }); - this.users = this.collection.findOne(usersId); - super.updateRecord({ 'count.users': tempUsers.length }); - super.addCountToTotal(tempUsers.length); + increaseCount(); + }); + + super.addCountToTotal(messagesCount); + ImporterWebsocket.progressUpdated({ rate: 100 }); if (hasDirectMessages) { tempChannels.push({ @@ -119,41 +156,10 @@ export class CsvImporter extends Base { } // Insert the channels records. - const channelsId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'channels', channels: tempChannels }); - this.channels = this.collection.findOne(channelsId); + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'channels', channels: tempChannels }); super.updateRecord({ 'count.channels': tempChannels.length }); super.addCountToTotal(tempChannels.length); - // Save the messages records to the import record for `startImport` usage - super.updateProgress(ProgressStep.PREPARING_MESSAGES); - let messagesCount = 0; - for (const [channel, messagesMap] of tempMessages.entries()) { - if (!this.messages.get(channel)) { - this.messages.set(channel, new Map()); - } - - for (const [msgGroupData, msgs] of messagesMap.entries()) { - messagesCount += msgs.length; - const channelName = `${ channel }/${ msgGroupData }`; - - super.updateRecord({ messagesstatus: channelName }); - - - if (Base.getBSONSize(msgs) > Base.getMaxBSONSize()) { - Base.getBSONSafeArraysFromAnArray(msgs).forEach((splitMsg, i) => { - const messagesId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channelName }.${ i }`, messages: splitMsg }); - this.messages.get(channel).set(`${ msgGroupData }.${ i }`, this.collection.findOne(messagesId)); - }); - } else { - const messagesId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: channelName, messages: msgs }); - this.messages.get(channel).set(msgGroupData, this.collection.findOne(messagesId)); - } - } - } - - super.updateRecord({ 'count.messages': messagesCount, messagesstatus: null }); - super.addCountToTotal(messagesCount); - // Ensure we have at least a single user, channel, or message if (tempUsers.length === 0 && tempChannels.length === 0 && messagesCount === 0) { this.logger.error('No users, channels, or messages found in the import file.'); @@ -170,6 +176,13 @@ export class CsvImporter extends Base { } startImport(importSelection) { + this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' }); + this.channels = RawImports.findOne({ import: this.importRecord._id, type: 'channels' }); + this.reloadCount(); + + const rawCollection = this.collection.model.rawCollection(); + const distinct = Meteor.wrapAsync(rawCollection.distinct, rawCollection); + super.startImport(importSelection); const started = Date.now(); @@ -274,7 +287,8 @@ export class CsvImporter extends Base { // If no channels file, collect channel map from DB for message-only import if (this.channels.channels.length === 0) { - for (const cname of this.messages.keys()) { + const channelNames = distinct('channel', { import: this.importRecord._id, type: 'messages', channel: { $ne: 'directMessages' } }); + for (const cname of channelNames) { Meteor.runAsUser(startedByUserId, () => { const existantRoom = Rooms.findOneByName(cname); if (existantRoom || cname.toUpperCase() === 'GENERAL') { @@ -291,23 +305,16 @@ export class CsvImporter extends Base { // If no users file, collect user map from DB for message-only import if (this.users.users.length === 0) { - for (const [ch, messagesMap] of this.messages.entries()) { - const csvChannel = this.getChannelFromName(ch); - if (!csvChannel || !csvChannel.do_import) { - continue; - } + const usernames = distinct('messages.username', { import: this.importRecord._id, type: 'messages' }); + for (const username of usernames) { Meteor.runAsUser(startedByUserId, () => { - for (const msgs of messagesMap.values()) { - for (const msg of msgs.messages) { - if (!this.getUserFromUsername(msg.username)) { - const user = Users.findOneByUsernameIgnoringCase(msg.username); - if (user) { - this.users.users.push({ - rocketId: user._id, - username: user.username, - }); - } - } + if (!this.getUserFromUsername(username)) { + const user = Users.findOneByUsernameIgnoringCase(username); + if (user) { + this.users.users.push({ + rocketId: user._id, + username: user.username, + }); } } }); @@ -316,61 +323,65 @@ export class CsvImporter extends Base { // Import the Messages super.updateProgress(ProgressStep.IMPORTING_MESSAGES); - for (const [ch, messagesMap] of this.messages.entries()) { + + const messagePacks = this.collection.find({ import: this.importRecord._id, type: 'messages' }); + messagePacks.forEach((pack) => { + const ch = pack.channel; + const { msgGroupData } = pack; + const csvChannel = this.getChannelFromName(ch); if (!csvChannel || !csvChannel.do_import) { - continue; + return; } if (csvChannel.isDirect) { - this._importDirectMessagesFile(messagesMap, startedByUserId); - continue; + this._importDirectMessagesFile(msgGroupData, pack, startedByUserId); + return; } if (ch.toLowerCase() === 'directmessages') { - continue; + return; } const room = Rooms.findOneById(csvChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); + const timestamps = {}; + Meteor.runAsUser(startedByUserId, () => { - const timestamps = {}; - for (const [msgGroupData, msgs] of messagesMap.entries()) { - super.updateRecord({ messagesstatus: `${ ch }/${ msgGroupData }.${ msgs.messages.length }` }); - for (const msg of msgs.messages) { - if (isNaN(new Date(parseInt(msg.ts)))) { - this.logger.warn(`Timestamp on a message in ${ ch }/${ msgGroupData } is invalid`); - super.addCountCompleted(1); - continue; - } + super.updateRecord({ messagesstatus: `${ ch }/${ msgGroupData }.${ pack.messages.length }` }); + for (const msg of pack.messages) { + if (isNaN(new Date(parseInt(msg.ts)))) { + this.logger.warn(`Timestamp on a message in ${ ch }/${ msgGroupData } is invalid`); + super.addCountCompleted(1); + continue; + } - const creator = this.getUserFromUsername(msg.username); - if (creator) { - let suffix = ''; - if (timestamps[msg.ts] === undefined) { - timestamps[msg.ts] = 1; - } else { - suffix = `-${ timestamps[msg.ts] }`; - timestamps[msg.ts] += 1; - } - const msgObj = { - _id: `csv-${ csvChannel.id }-${ msg.ts }${ suffix }`, - ts: new Date(parseInt(msg.ts)), - msg: msg.text, - rid: room._id, - u: { - _id: creator._id, - username: creator.username, - }, - }; - - sendMessage(creator, msgObj, room, true); + const creator = this.getUserFromUsername(msg.username); + if (creator) { + let suffix = ''; + if (timestamps[msg.ts] === undefined) { + timestamps[msg.ts] = 1; + } else { + suffix = `-${ timestamps[msg.ts] }`; + timestamps[msg.ts] += 1; } - - super.addCountCompleted(1); + const msgObj = { + _id: `csv-${ csvChannel.id }-${ msg.ts }${ suffix }`, + ts: new Date(parseInt(msg.ts)), + msg: msg.text, + rid: room._id, + u: { + _id: creator._id, + username: creator.username, + }, + }; + + insertMessage(creator, msgObj, room, true); } + + super.addCountCompleted(1); } }); - } + }); super.updateProgress(ProgressStep.FINISHING); super.updateProgress(ProgressStep.DONE); @@ -386,7 +397,7 @@ export class CsvImporter extends Base { return super.getProgress(); } - _importDirectMessagesFile(messagesMap, startedByUserId) { + _importDirectMessagesFile(msgGroupData, msgs, startedByUserId) { const dmUsers = {}; const findUser = (username) => { @@ -400,69 +411,59 @@ export class CsvImporter extends Base { Meteor.runAsUser(startedByUserId, () => { const timestamps = {}; - for (const [msgGroupData, msgs] of messagesMap.entries()) { - let room; - let rid; - super.updateRecord({ messagesstatus: `${ t('Direct_Messagest') }/${ msgGroupData }.${ msgs.messages.length }` }); - for (const msg of msgs.messages) { - if (isNaN(new Date(parseInt(msg.ts)))) { - this.logger.warn(`Timestamp on a message in ${ t('Direct_Messagest') }/${ msgGroupData } is invalid`); - super.addCountCompleted(1); - continue; - } - - const creator = findUser(msg.username); - const targetUser = findUser(msg.otherUsername); + let room; + let rid; + super.updateRecord({ messagesstatus: `${ t('Direct_Messagest') }/${ msgGroupData }.${ msgs.messages.length }` }); + for (const msg of msgs.messages) { + if (isNaN(new Date(parseInt(msg.ts)))) { + this.logger.warn(`Timestamp on a message in ${ t('Direct_Messagest') }/${ msgGroupData } is invalid`); + super.addCountCompleted(1); + continue; + } - if (creator && targetUser) { - if (!rid) { - const roomInfo = Meteor.runAsUser(creator._id, () => Meteor.call('createDirectMessage', targetUser.username)); - rid = roomInfo.rid; - room = Rooms.findOneById(rid, { fields: { usernames: 1, t: 1, name: 1 } }); - } + const creator = findUser(msg.username); + const targetUser = findUser(msg.otherUsername); - if (!room) { - this.logger.warn(`DM room not found for users ${ msg.username } and ${ msg.otherUsername }`); - super.addCountCompleted(1); - continue; - } + if (creator && targetUser) { + if (!rid) { + const roomInfo = Meteor.runAsUser(creator._id, () => Meteor.call('createDirectMessage', targetUser.username)); + rid = roomInfo.rid; + room = Rooms.findOneById(rid, { fields: { usernames: 1, t: 1, name: 1 } }); + } - let suffix = ''; - if (timestamps[msg.ts] === undefined) { - timestamps[msg.ts] = 1; - } else { - suffix = `-${ timestamps[msg.ts] }`; - timestamps[msg.ts] += 1; - } + if (!room) { + this.logger.warn(`DM room not found for users ${ msg.username } and ${ msg.otherUsername }`); + super.addCountCompleted(1); + continue; + } - const msgObj = { - _id: `csv-${ rid }-${ msg.ts }${ suffix }`, - ts: new Date(parseInt(msg.ts)), - msg: msg.text, - rid: room._id, - u: { - _id: creator._id, - username: creator.username, - }, - }; - - sendMessage(creator, msgObj, room, true); + let suffix = ''; + if (timestamps[msg.ts] === undefined) { + timestamps[msg.ts] = 1; + } else { + suffix = `-${ timestamps[msg.ts] }`; + timestamps[msg.ts] += 1; } - super.addCountCompleted(1); + const msgObj = { + _id: `csv-${ rid }-${ msg.ts }${ suffix }`, + ts: new Date(parseInt(msg.ts)), + msg: msg.text, + rid: room._id, + u: { + _id: creator._id, + username: creator.username, + }, + }; + + insertMessage(creator, msgObj, room, true); } + + super.addCountCompleted(1); } }); } - getSelection() { - const selectionUsers = this.users.users.map((u) => new SelectionUser(u.id, u.username, u.email, false, false, true)); - const selectionChannels = this.channels.channels.map((c) => new SelectionChannel(c.id, c.name, false, true, c.isPrivate)); - const selectionMessages = this.importRecord.count.messages; - - return new Selection(this.name, selectionUsers, selectionChannels, selectionMessages); - } - getChannelFromName(channelName) { if (channelName.toLowerCase() === 'directmessages') { return this.getDirectMessagesChannel(); diff --git a/app/importer-hipchat-enterprise/server/importer.js b/app/importer-hipchat-enterprise/server/importer.js index 768f13a961eb9..6203150012709 100644 --- a/app/importer-hipchat-enterprise/server/importer.js +++ b/app/importer-hipchat-enterprise/server/importer.js @@ -14,9 +14,8 @@ import { Selection, SelectionChannel, SelectionUser, - Imports, } from '../../importer/server'; -import { Messages, Users, Subscriptions, Rooms } from '../../models'; +import { Messages, Users, Subscriptions, Rooms, Imports } from '../../models'; import { insertMessage } from '../../lib'; const turndownService = new TurndownService({ @@ -36,8 +35,8 @@ turndownService.addRule('strikethrough', { }); export class HipChatEnterpriseImporter extends Base { - constructor(info) { - super(info); + constructor(info, importRecord) { + super(info, importRecord); this.Readable = Readable; this.zlib = require('zlib'); @@ -874,6 +873,7 @@ export class HipChatEnterpriseImporter extends Base { } startImport(importSelection) { + this.reloadCount(); super.startImport(importSelection); this._userDataCache = {}; const started = Date.now(); @@ -1281,26 +1281,6 @@ export class HipChatEnterpriseImporter extends Base { }); } - getSelection() { - const tempUsers = this.collection.findOne({ - import: this.importRecord._id, - importer: this.name, - type: 'users', - }); - - const tempChannels = this.collection.findOne({ - import: this.importRecord._id, - importer: this.name, - type: 'channels', - }); - - const selectionUsers = tempUsers.users.map((u) => new SelectionUser(u.id, u.username, u.email, u.isDeleted, false, u.do_import !== false, u.is_email_taken === true)); - const selectionChannels = tempChannels.channels.map((r) => new SelectionChannel(r.id, r.name, r.isArchived, true, r.isPrivate, r.creator)); - const selectionMessages = this.importRecord.count.messages; - - return new Selection(this.name, selectionUsers, selectionChannels, selectionMessages); - } - _getBasicUserData(userId) { if (this._userDataCache[userId]) { return this._userDataCache[userId]; diff --git a/app/importer-hipchat/server/importer.js b/app/importer-hipchat/server/importer.js index d1aebcfe59a72..49f6b8e2e4d0c 100644 --- a/app/importer-hipchat/server/importer.js +++ b/app/importer-hipchat/server/importer.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import moment from 'moment'; import { + RawImports, Base, ProgressStep, Selection, @@ -18,8 +19,8 @@ import { sendMessage } from '../../lib'; import 'moment-timezone'; export class HipChatImporter extends Base { - constructor(info) { - super(info); + constructor(info, importRecord) { + super(info, importRecord); this.userTags = []; this.roomPrefix = 'hipchat_export/rooms/'; @@ -99,35 +100,40 @@ export class HipChatImporter extends Base { this.addCountToTotal(tempRooms.length); super.updateProgress(ProgressStep.PREPARING_MESSAGES); let messagesCount = 0; + Object.keys(tempMessages).forEach((channel) => { const messagesObj = tempMessages[channel]; - this.messages[channel] = this.messages[channel] || {}; + Object.keys(messagesObj).forEach((date) => { const msgs = messagesObj[date]; messagesCount += msgs.length; this.updateRecord({ messagesstatus: `${ channel }/${ date }`, }); + if (Base.getBSONSize(msgs) > Base.getMaxBSONSize()) { Base.getBSONSafeArraysFromAnArray(msgs).forEach((splitMsg, i) => { - const messagesId = this.collection.insert({ + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }.${ i }`, messages: splitMsg, + channel, + date, + i, }); - this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); }); } else { - const messagesId = this.collection.insert({ + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }`, messages: msgs, + channel, + date, }); - this.messages[channel][date] = this.collection.findOne(messagesId); } }); }); @@ -153,6 +159,10 @@ export class HipChatImporter extends Base { } startImport(importSelection) { + this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' }); + this.channels = RawImports.findOne({ import: this.importRecord._id, type: 'channels' }); + this.reloadCount(); + super.startImport(importSelection); const start = Date.now(); @@ -222,11 +232,15 @@ export class HipChatImporter extends Base { this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } }); + const channelNames = []; + super.updateProgress(ProgressStep.IMPORTING_CHANNELS); this.channels.channels.forEach((channel) => { if (!channel.do_import) { return; } + + channelNames.push(channel.name); Meteor.runAsUser(startedByUserId, () => { channel.name = channel.name.replace(/ /g, ''); const existantRoom = Rooms.findOneByName(channel.name); @@ -264,50 +278,53 @@ export class HipChatImporter extends Base { super.updateProgress(ProgressStep.IMPORTING_MESSAGES); const nousers = {}; - Object.keys(this.messages).forEach((channel) => { - const messagesObj = this.messages[channel]; - Meteor.runAsUser(startedByUserId, () => { - const hipchatChannel = this.getHipChatChannelFromName(channel); - if (hipchatChannel != null ? hipchatChannel.do_import : undefined) { - const room = Rooms.findOneById(hipchatChannel.rocketId, { - fields: { - usernames: 1, - t: 1, - name: 1, - }, - }); + for (const channel of channelNames) { + const hipchatChannel = this.getHipChatChannelFromName(channel); - Object.keys(messagesObj).forEach((date) => { - const msgs = messagesObj[date]; - this.updateRecord({ - messagesstatus: `${ channel }/${ date }.${ msgs.messages.length }`, - }); + if (!hipchatChannel || !hipchatChannel.do_import) { + continue; + } - msgs.messages.forEach((message) => { - if (message.from != null) { - const user = this.getRocketUser(message.from.user_id); - if (user != null) { - const msgObj = { - msg: this.convertHipChatMessageToRocketChat(message.message), - ts: new Date(message.date), - u: { - _id: user._id, - username: user.username, - }, - }; - sendMessage(user, msgObj, room, true); - } else if (!nousers[message.from.user_id]) { - nousers[message.from.user_id] = message.from; - } - } else if (!_.isArray(message)) { - console.warn('Please report the following:', message); + const room = Rooms.findOneById(hipchatChannel.rocketId, { + fields: { + usernames: 1, + t: 1, + name: 1, + }, + }); + + const messagePacks = this.collection.find({ import: this.importRecord._id, type: 'messages', channel }); + + Meteor.runAsUser(startedByUserId, () => { + messagePacks.forEach((pack) => { + const packId = pack.i ? `${ pack.date }.${ pack.i }` : pack.date; + + this.updateRecord({ messagesstatus: `${ channel }/${ packId } (${ pack.messages.length })` }); + pack.messages.forEach((message) => { + if (message.from != null) { + const user = this.getRocketUser(message.from.user_id); + if (user != null) { + const msgObj = { + msg: this.convertHipChatMessageToRocketChat(message.message), + ts: new Date(message.date), + u: { + _id: user._id, + username: user.username, + }, + }; + sendMessage(user, msgObj, room, true); + } else if (!nousers[message.from.user_id]) { + nousers[message.from.user_id] = message.from; } - this.addCountCompleted(1); - }); + } else if (!_.isArray(message)) { + console.warn('Please report the following:', message); + } + + this.addCountCompleted(1); }); - } + }); }); - }); + } this.logger.warn('The following did not have users:', nousers); super.updateProgress(ProgressStep.FINISHING); @@ -355,14 +372,4 @@ export class HipChatImporter extends Base { } return message; } - - getSelection() { - const selectionUsers = this.users.users.map(function(user) { - return new SelectionUser(user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot); - }); - const selectionChannels = this.channels.channels.map(function(room) { - return new SelectionChannel(room.room_id, room.name, room.is_archived, true, false); - }); - return new Selection(this.name, selectionUsers, selectionChannels, this.importRecord.count.messages); - } } diff --git a/app/importer-pending-files/server/importer.js b/app/importer-pending-files/server/importer.js new file mode 100644 index 0000000000000..5de6f66aeaf83 --- /dev/null +++ b/app/importer-pending-files/server/importer.js @@ -0,0 +1,202 @@ +import https from 'https'; +import http from 'http'; + +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; + +import { + Base, + ProgressStep, + Selection, +} from '../../importer/server'; +import { Messages } from '../../models'; +import { FileUpload } from '../../file-upload'; + +export class PendingFileImporter extends Base { + constructor(info, importRecord) { + super(info, importRecord); + this.userTags = []; + this.bots = {}; + } + + prepareFileCount() { + this.logger.debug('start preparing import operation'); + super.updateProgress(ProgressStep.PREPARING_STARTED); + + const messages = Messages.findAllImportedMessagesWithFilesToDownload(); + const fileCount = messages.count(); + + if (fileCount === 0) { + super.updateProgress(ProgressStep.DONE); + return 0; + } + + this.updateRecord({ 'count.messages': fileCount, messagesstatus: null }); + this.addCountToTotal(fileCount); + + const fileData = new Selection(this.name, [], [], fileCount); + this.updateRecord({ fileData }); + + super.updateProgress(ProgressStep.IMPORTING_FILES); + Meteor.defer(() => { + this.startImport(fileData); + }); + + return fileCount; + } + + startImport() { + const pendingFileMessageList = Messages.findAllImportedMessagesWithFilesToDownload(); + const downloadedFileIds = []; + const maxFileCount = 10; + const maxFileSize = 1024 * 1024 * 500; + + let count = 0; + let currentSize = 0; + let nextSize = 0; + + const waitForFiles = () => { + if ((count + 1) < maxFileCount && (currentSize + nextSize) < maxFileSize) { + return; + } + + Meteor.wrapAsync((callback) => { + const handler = setInterval(() => { + if ((count + 1) >= maxFileCount) { + return; + } + + if (currentSize + nextSize >= maxFileSize && count > 0) { + return; + } + + clearInterval(handler); + callback(); + }, 1000); + })(); + }; + + const completeFile = (details) => { + this.addCountCompleted(1); + count--; + currentSize -= details.size; + }; + + const logError = (error) => { + this.logger.error(error); + }; + + try { + pendingFileMessageList.forEach((message) => { + try { + const { _importFile } = message; + + if (!_importFile || _importFile.downloaded || downloadedFileIds.includes(_importFile.id)) { + this.addCountCompleted(1); + return; + } + + const url = _importFile.downloadUrl; + if (!url || !url.startsWith('http')) { + this.addCountCompleted(1); + return; + } + + const details = { + message_id: `${ message._id }-file-${ _importFile.id }`, + name: _importFile.name || Random.id(), + size: _importFile.size || 0, + userId: message.u._id, + rid: message.rid, + }; + + const requestModule = /https/i.test(url) ? https : http; + const fileStore = FileUpload.getStore('Uploads'); + const reportProgress = this.reportProgress.bind(this); + + nextSize = details.size; + waitForFiles(); + count++; + currentSize += nextSize; + downloadedFileIds.push(_importFile.id); + + requestModule.get(url, Meteor.bindEnvironment(function(res) { + const contentType = res.headers['content-type']; + if (!details.type && contentType) { + details.type = contentType; + } + + const rawData = []; + res.on('data', Meteor.bindEnvironment((chunk) => { + rawData.push(chunk); + + // Update progress more often on large files + reportProgress(); + })); + res.on('error', Meteor.bindEnvironment((error) => { + completeFile(details); + logError(error); + })); + + res.on('end', Meteor.bindEnvironment(() => { + try { + // Bypass the fileStore filters + fileStore._doInsert(details, Buffer.concat(rawData), function(error, file) { + if (error) { + completeFile(details); + logError(error); + return; + } + + const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`); + const attachment = { + title: file.name, + title_link: url, + }; + + if (/^image\/.+/.test(file.type)) { + attachment.image_url = url; + attachment.image_type = file.type; + attachment.image_size = file.size; + attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; + } + + if (/^audio\/.+/.test(file.type)) { + attachment.audio_url = url; + attachment.audio_type = file.type; + attachment.audio_size = file.size; + } + + if (/^video\/.+/.test(file.type)) { + attachment.video_url = url; + attachment.video_type = file.type; + attachment.video_size = file.size; + } + + Messages.setImportFileRocketChatAttachment(_importFile.id, url, attachment); + completeFile(details); + }); + } catch (error) { + completeFile(details); + logError(error); + } + })); + })); + } catch (error) { + this.logger.error(error); + } + }); + } catch (error) { + // If the cursor expired, restart the method + if (error && error.codeName === 'CursorNotFound') { + return this.startImport(); + } + + super.updateProgress(ProgressStep.ERROR); + throw error; + } + + super.updateProgress(ProgressStep.DONE); + return this.getProgress(); + } +} diff --git a/app/importer-pending-files/server/index.js b/app/importer-pending-files/server/index.js new file mode 100644 index 0000000000000..9a8117ea80997 --- /dev/null +++ b/app/importer-pending-files/server/index.js @@ -0,0 +1,5 @@ +import { PendingFileImporter } from './importer'; +import { Importers } from '../../importer/server'; +import { PendingFileImporterInfo } from './info'; + +Importers.add(new PendingFileImporterInfo(), PendingFileImporter); diff --git a/app/importer-pending-files/server/info.js b/app/importer-pending-files/server/info.js new file mode 100644 index 0000000000000..f12d01a526112 --- /dev/null +++ b/app/importer-pending-files/server/info.js @@ -0,0 +1,7 @@ +import { ImporterInfo } from '../../importer/lib/ImporterInfo'; + +export class PendingFileImporterInfo extends ImporterInfo { + constructor() { + super('pending-files', 'Pending Files', ''); + } +} diff --git a/app/importer-slack-users/server/importer.js b/app/importer-slack-users/server/importer.js index 6371a568f4a95..0024842e43612 100644 --- a/app/importer-slack-users/server/importer.js +++ b/app/importer-slack-users/server/importer.js @@ -3,6 +3,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; import { + RawImports, Base, ProgressStep, Selection, @@ -12,8 +13,8 @@ import { RocketChatFile } from '../../file'; import { Users } from '../../models'; export class SlackUsersImporter extends Base { - constructor(info) { - super(info); + constructor(info, importRecord) { + super(info, importRecord); this.csvParser = require('csv-parse/lib/sync'); this.userMap = new Map(); @@ -68,11 +69,35 @@ export class SlackUsersImporter extends Base { return super.getProgress(); } + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'admins', admins: this.admins }); + super.updateProgress(ProgressStep.USER_SELECTION); return new Selection(this.name, userArray, [], 0); } startImport(importSelection) { + const admins = this.collection.findOne({ import: this.importRecord._id, type: 'admins' }); + if (admins) { + this.admins = admins.admins || []; + } else { + this.admins = []; + } + + this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' }); + // Recreate the userMap from the collection data + this.userMap = new Map(); + for (const user of this.users.users) { + const obj = new SelectionUser(); + for (const propName in user) { + if (user.hasOwnProperty(propName)) { + obj[propName] = user[propName]; + } + } + this.userMap.set(user.user_id, obj); + } + + this.reloadCount(); + super.startImport(importSelection); const started = Date.now(); diff --git a/app/importer-slack/server/importer.js b/app/importer-slack/server/importer.js index 08fc7e913ec82..8cbfb213f5753 100644 --- a/app/importer-slack/server/importer.js +++ b/app/importer-slack/server/importer.js @@ -3,133 +3,197 @@ import { Accounts } from 'meteor/accounts-base'; import _ from 'underscore'; import { + RawImports, Base, ProgressStep, Selection, SelectionChannel, SelectionUser, + ImporterWebsocket, } from '../../importer/server'; -import { RocketChatFile } from '../../file'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { Users, Rooms, Messages } from '../../models'; -import { sendMessage } from '../../lib'; - +import { insertMessage } from '../../lib'; +import { getValidRoomName } from '../../utils'; export class SlackImporter extends Base { - constructor(info) { - super(info); + constructor(info, importRecord) { + super(info, importRecord); this.userTags = []; this.bots = {}; } - prepare(dataURI, sentContentType, fileName) { - super.prepare(dataURI, sentContentType, fileName); + parseData(data) { + const dataString = data.toString(); + try { + this.logger.debug('parsing file contents'); + return JSON.parse(dataString); + } catch (e) { + console.error(e); + return false; + } + } - const { image } = RocketChatFile.dataURIParse(dataURI); - const zip = new this.AdmZip(new Buffer(image, 'base64')); - const zipEntries = zip.getEntries(); + prepareUsingLocalFile(fullFilePath) { + this.logger.debug('start preparing import operation'); + this.collection.remove({}); + + const zip = new this.AdmZip(fullFilePath); + const totalEntries = zip.getEntryCount(); let tempChannels = []; + let tempGroups = []; + let tempMpims = []; + let tempDMs = []; let tempUsers = []; - const tempMessages = {}; + let messagesCount = 0; + let count = 0; - zipEntries.forEach((entry) => { - if (entry.entryName.indexOf('__MACOSX') > -1) { - return this.logger.debug(`Ignoring the file: ${ entry.entryName }`); - } + ImporterWebsocket.progressUpdated({ rate: 0 }); + let oldRate = 0; - if (entry.entryName === 'channels.json') { - super.updateProgress(ProgressStep.PREPARING_CHANNELS); - tempChannels = JSON.parse(entry.getData().toString()).filter((channel) => channel.creator != null); - return; + const prepareChannelsFile = (entry, typeName, filterInvalidCreators = true) => { + super.updateProgress(ProgressStep.PREPARING_CHANNELS); + let data = JSON.parse(entry.getData().toString()); + + if (filterInvalidCreators) { + data = data.filter((channel) => channel.creator != null); } - if (entry.entryName === 'users.json') { - super.updateProgress(ProgressStep.PREPARING_USERS); - tempUsers = JSON.parse(entry.getData().toString()); + this.logger.debug(`loaded ${ data.length } ${ typeName }.`); - tempUsers.forEach((user) => { - if (user.is_bot) { - this.bots[user.profile.bot_id] = user; + // Insert the channels records. + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: typeName, channels: data }); + this.updateRecord({ 'count.channels': tempGroups.length + tempChannels.length + tempDMs.length + tempMpims.length + data.length }); + this.addCountToTotal(data.length); + return data; + }; + + try { + zip.forEach((entry) => { + try { + if (entry.entryName.includes('__MACOSX') || entry.entryName.includes('.DS_Store')) { + return this.logger.debug(`Ignoring the file: ${ entry.entryName }`); } - }); - return; - } + if (entry.entryName === 'channels.json') { + tempChannels = prepareChannelsFile(entry, 'channels'); + return; + } + + if (entry.entryName === 'groups.json') { + tempGroups = prepareChannelsFile(entry, 'groups'); + return; + } - if (!entry.isDirectory && entry.entryName.indexOf('/') > -1) { - const item = entry.entryName.split('/'); - const channelName = item[0]; - const msgGroupData = item[1].split('.')[0]; - tempMessages[channelName] = tempMessages[channelName] || {}; + if (entry.entryName === 'mpims.json') { + tempMpims = prepareChannelsFile(entry, 'mpims'); + return; + } - try { - tempMessages[channelName][msgGroupData] = JSON.parse(entry.getData().toString()); - } catch (error) { - this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); - } - } - }); + if (entry.entryName === 'dms.json') { + tempDMs = prepareChannelsFile(entry, 'DMs', false); + return; + } - // Insert the users record, eventually this might have to be split into several ones as well - // if someone tries to import a several thousands users instance - const usersId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers }); - this.users = this.collection.findOne(usersId); - this.updateRecord({ 'count.users': tempUsers.length }); - this.addCountToTotal(tempUsers.length); + if (entry.entryName === 'users.json') { + super.updateProgress(ProgressStep.PREPARING_USERS); + tempUsers = JSON.parse(entry.getData().toString()); - // Insert the channels records. - const channelsId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'channels', channels: tempChannels }); - this.channels = this.collection.findOne(channelsId); - this.updateRecord({ 'count.channels': tempChannels.length }); - this.addCountToTotal(tempChannels.length); + tempUsers.forEach((user) => { + if (user.is_bot) { + this.bots[user.profile.bot_id] = user; + } + }); - // Insert the messages records - super.updateProgress(ProgressStep.PREPARING_MESSAGES); + this.logger.debug(`loaded ${ tempUsers.length } users.`); - let messagesCount = 0; - Object.keys(tempMessages).forEach((channel) => { - const messagesObj = tempMessages[channel]; - this.messages[channel] = this.messages[channel] || {}; - - Object.keys(messagesObj).forEach((date) => { - const msgs = messagesObj[date]; - messagesCount += msgs.length; - this.updateRecord({ messagesstatus: `${ channel }/${ date }` }); - if (Base.getBSONSize(msgs) > Base.getMaxBSONSize()) { - const tmp = Base.getBSONSafeArraysFromAnArray(msgs); - Object.keys(tmp).forEach((i) => { - const splitMsg = tmp[i]; - const messagesId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }.${ i }`, messages: splitMsg }); - this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); - }); - } else { - const messagesId = this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }`, messages: msgs }); - this.messages[channel][date] = this.collection.findOne(messagesId); + // Insert the users record + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers }); + this.updateRecord({ 'count.users': tempUsers.length }); + this.addCountToTotal(tempUsers.length); + + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'bots', bots: this.bots }); + return; + } + + if (!entry.isDirectory && entry.entryName.indexOf('/') > -1) { + const item = entry.entryName.split('/'); + + const channel = item[0]; + const date = item[1].split('.')[0]; + + try { + // Insert the messages records + if (this.progress.step !== ProgressStep.PREPARING_MESSAGES) { + super.updateProgress(ProgressStep.PREPARING_MESSAGES); + } + + const tempMessages = JSON.parse(entry.getData().toString()); + messagesCount += tempMessages.length; + this.updateRecord({ messagesstatus: `${ channel }/${ date }` }); + this.addCountToTotal(tempMessages.length); + + if (Base.getBSONSize(tempMessages) > Base.getMaxBSONSize()) { + const tmp = Base.getBSONSafeArraysFromAnArray(tempMessages); + Object.keys(tmp).forEach((i) => { + const splitMsg = tmp[i]; + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }.${ i }`, messages: splitMsg, channel, date, i }); + }); + } else { + this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channel }/${ date }`, messages: tempMessages, channel, date }); + } + } catch (error) { + this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); + } + } + } catch (e) { + this.logger.error(e); + } + + try { + count++; + const rate = Math.floor(count * 1000 / totalEntries) / 10; + if (rate > oldRate) { + ImporterWebsocket.progressUpdated({ rate }); + oldRate = rate; + } + } catch (e) { + console.error(e); } }); - }); + } catch (e) { + this.logger.error(e); + throw e; + } + ImporterWebsocket.progressUpdated({ rate: 100 }); this.updateRecord({ 'count.messages': messagesCount, messagesstatus: null }); - this.addCountToTotal(messagesCount); - if ([tempUsers.length, tempChannels.length, messagesCount].some((e) => e === 0)) { - this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); - console.log(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + if ([tempUsers.length, tempChannels.length + tempGroups.length + tempDMs.length + tempMpims.length, messagesCount].some((e) => e === 0)) { + this.logger.warn(`Loaded ${ tempUsers.length } users, ${ tempChannels.length } channels, ${ tempGroups.length } groups, ${ tempDMs.length } DMs, ${ tempMpims.length } multi party IMs and ${ messagesCount } messages`); super.updateProgress(ProgressStep.ERROR); return this.getProgress(); } const selectionUsers = tempUsers.map((user) => new SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); const selectionChannels = tempChannels.map((channel) => new SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + const selectionGroups = tempGroups.map((channel) => new SelectionChannel(channel.id, channel.name, channel.is_archived, true, true)); + const selectionMpims = tempMpims.map((channel) => new SelectionChannel(channel.id, channel.name, channel.is_archived, true, true)); + const selectionMessages = this.importRecord.count.messages; super.updateProgress(ProgressStep.USER_SELECTION); - return new Selection(this.name, selectionUsers, selectionChannels, selectionMessages); + return new Selection(this.name, selectionUsers, selectionChannels.concat(selectionGroups).concat(selectionMpims), selectionMessages); } performUserImport(user, startedByUserId) { + if (user.is_bot) { + this._saveUserIdReference(user.id, 'rocket.cat', user.name, 'rocket.cat'); + } + if (!user.do_import) { + this.addCountCompleted(1); return; } @@ -138,11 +202,7 @@ export class SlackImporter extends Base { if (existantUser) { user.rocketId = existantUser._id; Users.update({ _id: user.rocketId }, { $addToSet: { importIds: user.id } }); - this.userTags.push({ - slack: `<@${ user.id }>`, - slackLong: `<@${ user.id }|${ user.name }>`, - rocket: `@${ existantUser.username }`, - }); + this._saveUserIdReference(user.id, existantUser._id, user.name, existantUser.username); } else { const userId = user.profile.email ? Accounts.createUser({ email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() }) : Accounts.createUser({ username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true }); Meteor.runAsUser(userId, () => { @@ -174,11 +234,7 @@ export class SlackImporter extends Base { } user.rocketId = userId; - this.userTags.push({ - slack: `<@${ user.id }>`, - slackLong: `<@${ user.id }|${ user.name }>`, - rocket: `@${ user.name }`, - }); + this._saveUserIdReference(user.id, userId, user.name, user.name); } this.addCountCompleted(1); @@ -188,11 +244,11 @@ export class SlackImporter extends Base { processMessageSubType(message, room, msgDataDefaults, missedTypes) { const ignoreTypes = { bot_add: true, file_comment: true, file_mention: true }; - let rocketUser = this.getRocketUser(message.user); + let rocketUser = this.getRocketUserFromUserId(message.user); const useRocketCat = !rocketUser; if (useRocketCat) { - rocketUser = Users.findOneById('rocket.cat', { fields: { username: 1, name: 1 } }); + rocketUser = this.getRocketUserFromUserId('rocket.cat'); } if (!rocketUser) { @@ -201,11 +257,13 @@ export class SlackImporter extends Base { switch (message.subtype) { case 'channel_join': + case 'group_join': if (!useRocketCat) { Messages.createUserJoinWithRoomIdAndUser(room._id, rocketUser, msgDataDefaults); } break; case 'channel_leave': + case 'group_leave': if (!useRocketCat) { Messages.createUserLeaveWithRoomIdAndUser(room._id, rocketUser, msgDataDefaults); } @@ -215,25 +273,25 @@ export class SlackImporter extends Base { ...msgDataDefaults, msg: `_${ this.convertSlackMessageToRocketChat(message.text) }_`, }; - sendMessage(rocketUser, msgObj, room, true); + insertMessage(rocketUser, msgObj, room, this._anyExistingSlackMessage); break; } case 'bot_message': case 'slackbot_response': { - const botUser = Users.findOneById('rocket.cat', { fields: { username: 1 } }); + const botUser = this.getRocketUserFromUserId('rocket.cat'); const botUsername = this.bots[message.bot_id] ? this.bots[message.bot_id].name : message.username; const msgObj = { ...msgDataDefaults, msg: this.convertSlackMessageToRocketChat(message.text), rid: room._id, bot: true, - attachments: message.attachments, + attachments: this.convertMessageAttachments(message.attachments), username: botUsername || undefined, }; if (message.edited) { msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); - const editedBy = this.getRocketUser(message.edited.user); + const editedBy = this.getRocketUserFromUserId(message.edited.user); if (editedBy) { msgObj.editedBy = { _id: editedBy._id, @@ -245,17 +303,20 @@ export class SlackImporter extends Base { if (message.icons) { msgObj.emoji = message.icons.emoji; } - sendMessage(botUser, msgObj, room, true); + insertMessage(botUser, msgObj, room, this._anyExistingSlackMessage); break; } case 'channel_purpose': + case 'group_purpose': Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_description', room._id, message.purpose, rocketUser, msgDataDefaults); break; case 'channel_topic': + case 'group_topic': Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, rocketUser, msgDataDefaults); break; case 'channel_name': + case 'group_name': Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, rocketUser, msgDataDefaults); break; case 'pinned_item': @@ -273,7 +334,7 @@ export class SlackImporter extends Base { } else { // TODO: make this better this.logger.debug('Pinned item with no attachment, needs work.'); - // Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults + // Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUserFromUserId(message.user), msgDataDefaults } break; case 'file_share': @@ -310,12 +371,14 @@ export class SlackImporter extends Base { reaction.name = `:${ reaction.name }:`; msgDataDefaults.reactions[reaction.name] = { usernames: [] }; - reaction.users.forEach((u) => { - const rcUser = this.getRocketUser(u); - if (!rcUser) { return; } + if (reaction.users) { + reaction.users.forEach((u) => { + const rcUser = this.getRocketUserFromUserId(u); + if (!rcUser) { return; } - msgDataDefaults.reactions[reaction.name].usernames.push(rcUser.username); - }); + msgDataDefaults.reactions[reaction.name].usernames.push(rcUser.username); + }); + } if (msgDataDefaults.reactions[reaction.name].usernames.length === 0) { delete msgDataDefaults.reactions[reaction.name]; @@ -325,32 +388,31 @@ export class SlackImporter extends Base { if (message.type === 'message') { if (message.files) { - const fileUser = this.getRocketUser(message.user); + const fileUser = this.getRocketUserFromUserId(message.user); let fileIndex = 0; message.files.forEach((file) => { fileIndex++; - if (file.url_private_download === undefined) { - return; - } const msgObj = { _id: `slack-${ slackChannel.id }-${ message.ts.replace(/\./g, '-') }-file${ fileIndex }`, ts: msgDataDefaults.ts, - msg: file.url_private_download, + msg: file.url_private_download || '', + _importFile: this.convertSlackFileToPendingFile(file), }; - sendMessage(fileUser, msgObj, room, true); + insertMessage(fileUser, msgObj, room, this._anyExistingSlackMessage); }); } if (message.subtype) { this.processMessageSubType(message, room, msgDataDefaults, missedTypes); } else { - const user = this.getRocketUser(message.user); + const user = this.getRocketUserFromUserId(message.user); if (user) { const msgObj = { ...msgDataDefaults, msg: this.convertSlackMessageToRocketChat(message.text), rid: room._id, + attachments: this.convertMessageAttachments(message.attachments), u: { _id: user._id, username: user.username, @@ -359,7 +421,7 @@ export class SlackImporter extends Base { if (message.edited) { msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); - const editedBy = this.getRocketUser(message.edited.user); + const editedBy = this.getRocketUserFromUserId(message.edited.user); if (editedBy) { msgObj.editedBy = { _id: editedBy._id, @@ -369,7 +431,7 @@ export class SlackImporter extends Base { } try { - sendMessage(this.getRocketUser(message.user), msgObj, room, true); + insertMessage(this.getRocketUserFromUserId(message.user), msgObj, room, this._anyExistingSlackMessage); } catch (e) { this.logger.warn(`Failed to import the message: ${ msgDataDefaults._id }`); } @@ -380,10 +442,247 @@ export class SlackImporter extends Base { this.addCountCompleted(1); } - startImport(importSelection) { - super.startImport(importSelection); - const start = Date.now(); + _saveUserIdReference(slackId, rocketId, slackUsername, rocketUsername) { + this._userIdReference[slackId] = rocketId; + + this.userTags.push({ + slack: `<@${ slackId }>`, + slackLong: `<@${ slackId }|${ slackUsername }>`, + rocket: `@${ rocketUsername }`, + }); + } + + _getUserRocketId(slackId) { + if (!this._userIdReference) { + return; + } + + return this._userIdReference[slackId]; + } + + _importUsers(startedByUserId) { + this._userIdReference = {}; + + super.updateProgress(ProgressStep.IMPORTING_USERS); + this.users.users.forEach((user) => this.performUserImport(user, startedByUserId)); + this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } }); + } + + _importChannels(startedByUserId, channelNames) { + if (!this.channels || !this.channels.channels) { + return; + } + + super.updateProgress(ProgressStep.IMPORTING_CHANNELS); + this.channels.channels.forEach((channel) => { + if (!channel.do_import) { + this.addCountCompleted(1); + return; + } + + channelNames.push(channel.name); + + Meteor.runAsUser(startedByUserId, () => { + const existingRoom = this._findExistingRoom(channel.name); + + if (existingRoom || channel.is_general) { + if (channel.is_general && existingRoom && channel.name !== existingRoom.name) { + Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channel.name); + } + + channel.rocketId = channel.is_general ? 'GENERAL' : existingRoom._id; + Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + const users = this._getChannelUserList(channel); + const userId = this.getImportedRocketUserIdFromSlackUserId(channel.creator) || startedByUserId; + + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createChannel', channel.name, users); + channel.rocketId = returned.rid; + }); + + this._updateImportedChannelTopicAndDescription(channel); + } + + this.addCountCompleted(1); + }); + }); + + this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } }); + } + + _findExistingRoom(name) { + const existingRoom = Rooms.findOneByName(name); + + // If no room with that name is found, try applying name rules and searching again + if (!existingRoom) { + const newName = getValidRoomName(name, null, { allowDuplicates: true }); + + if (newName !== name) { + return Rooms.findOneByName(newName); + } + } + + return existingRoom; + } + + _getChannelUserList(channel) { + return channel.members.reduce((ret, member) => { + if (member !== channel.creator) { + const user = this.getRocketUserFromUserId(member); + // Don't add bots to the room's member list; Since they are all replaced with rocket.cat, it could cause duplicated subscriptions + if (user && user.username && user._id !== 'rocket.cat') { + ret.push(user.username); + } + } + return ret; + }, []); + } + + _importPrivateGroupList(startedByUserId, list, channelNames) { + if (!list || !list.channels) { + return; + } + + list.channels.forEach((channel) => { + if (!channel.do_import) { + this.addCountCompleted(1); + return; + } + + channelNames.push(channel.name); + + Meteor.runAsUser(startedByUserId, () => { + const existingRoom = this._findExistingRoom(channel.name); + + if (existingRoom) { + channel.rocketId = existingRoom._id; + Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + const users = this._getChannelUserList(channel); + + const userId = this.getImportedRocketUserIdFromSlackUserId(channel.creator) || startedByUserId; + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createPrivateGroup', channel.name, users); + channel.rocketId = returned.rid; + }); + + this._updateImportedChannelTopicAndDescription(channel); + } + + this.addCountCompleted(1); + }); + }); + + this.collection.update({ _id: list._id }, { $set: { channels: list.channels } }); + } + + _importGroups(startedByUserId, channelNames) { + this._importPrivateGroupList(startedByUserId, this.groups, channelNames); + } + + _updateImportedChannelTopicAndDescription(slackChannel) { + // @TODO implement model specific function + const roomUpdate = { + ts: new Date(slackChannel.created * 1000), + }; + + if (!_.isEmpty(slackChannel.topic && slackChannel.topic.value)) { + roomUpdate.topic = slackChannel.topic.value; + } + + if (!_.isEmpty(slackChannel.purpose && slackChannel.purpose.value)) { + roomUpdate.description = slackChannel.purpose.value; + } + Rooms.update({ _id: slackChannel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: slackChannel.id } }); + } + + _importMpims(startedByUserId, channelNames) { + this._importPrivateGroupList(startedByUserId, this.mpims, channelNames); + } + + _importDMs(startedByUserId, channelNames) { + if (!this.dms || !this.dms.channels) { + return; + } + + this.dms.channels.forEach((channel) => { + channelNames.push(channel.id); + + if (!channel.members || channel.members.length !== 2) { + this.addCountCompleted(1); + return; + } + + Meteor.runAsUser(startedByUserId, () => { + const userId1 = this.getImportedRocketUserIdFromSlackUserId(channel.members[0]); + const userId2 = this.getImportedRocketUserIdFromSlackUserId(channel.members[1]); + + const rid = [userId1, userId2].sort().join(''); + const existingRoom = Rooms.findOneById(rid); + + if (existingRoom) { + channel.rocketId = existingRoom._id; + Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + if (!userId1) { + this.logger.error(`DM creation: User not found for id ${ channel.members[0] } and channel id ${ channel.id }`); + return; + } + + if (!userId2) { + this.logger.error(`DM creation: User not found for id ${ channel.members[1] } and channel id ${ channel.id }`); + return; + } + + const user = this._getBasicUserData(userId2); + + if (!user) { + return; + } + + const roomInfo = Meteor.runAsUser(userId1, () => Meteor.call('createDirectMessage', user.username)); + channel.rocketId = roomInfo.rid; + Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } + + this.addCountCompleted(1); + }); + }); + + this.collection.update({ _id: this.dms._id }, { $set: { channels: this.dms.channels } }); + } + + _importMessages(startedByUserId, channelNames) { + const missedTypes = {}; + super.updateProgress(ProgressStep.IMPORTING_MESSAGES); + for (const channel of channelNames) { + const slackChannel = this.getSlackChannelFromName(channel); + + const room = Rooms.findOneById(slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); + if (!room) { + this.logger.error(`ROOM not found: ${ channel }`); + continue; + } + const messagePacks = this.collection.find({ import: this.importRecord._id, type: 'messages', channel }); + + Meteor.runAsUser(startedByUserId, () => { + messagePacks.forEach((pack) => { + const packId = pack.i ? `${ pack.date }.${ pack.i }` : pack.date; + + this.updateRecord({ messagesstatus: `${ channel }/${ packId } (${ pack.messages.length })` }); + pack.messages.forEach((message) => this.performMessageImport(message, room, missedTypes, slackChannel)); + }); + }); + } + + if (!_.isEmpty(missedTypes)) { + console.log('Missed import types:', missedTypes); + } + } + + _applyUserSelection(importSelection) { Object.keys(importSelection.users).forEach((key) => { const user = importSelection.users[key]; Object.keys(this.users.users).forEach((k) => { @@ -394,115 +693,103 @@ export class SlackImporter extends Base { }); }); this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } }); + } - Object.keys(importSelection.channels).forEach((key) => { - const channel = importSelection.channels[key]; - Object.keys(this.channels.channels).forEach((k) => { - const c = this.channels.channels[k]; - if (c.id === channel.channel_id) { - c.do_import = channel.do_import; + _applyChannelSelection(importSelection) { + const iterateChannelList = (list, channel_id, do_import) => { + Object.keys(list).forEach((k) => { + const c = list[k]; + if (c.id === channel_id) { + c.do_import = do_import; } }); + }; + + Object.keys(importSelection.channels).forEach((key) => { + const channel = importSelection.channels[key]; + + if (this.channels && this.channels.channels) { + iterateChannelList(this.channels.channels, channel.channel_id, channel.do_import); + } + + if (this.groups && this.groups.channels) { + iterateChannelList(this.groups.channels, channel.channel_id, channel.do_import); + } + + if (this.mpims && this.mpims.channels) { + iterateChannelList(this.mpims.channels, channel.channel_id, channel.do_import); + } }); - this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } }); - const startedByUserId = Meteor.userId(); - Meteor.defer(() => { - try { - super.updateProgress(ProgressStep.IMPORTING_USERS); - this.users.users.forEach((user) => this.performUserImport(user, startedByUserId)); - this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } }); + if (this.channels && this.channels.channels) { + this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } }); + } - super.updateProgress(ProgressStep.IMPORTING_CHANNELS); - this.channels.channels.forEach((channel) => { - if (!channel.do_import) { - return; - } + if (this.groups && this.groups.channels) { + this.collection.update({ _id: this.groups._id }, { $set: { channels: this.groups.channels } }); + } - Meteor.runAsUser(startedByUserId, () => { - const existantRoom = Rooms.findOneByName(channel.name); - if (existantRoom || channel.is_general) { - if (channel.is_general && existantRoom && channel.name !== existantRoom.name) { - Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channel.name); - } + if (this.mpims && this.mpims.channels) { + this.collection.update({ _id: this.mpims._id }, { $set: { channels: this.mpims.channels } }); + } + } - channel.rocketId = channel.is_general ? 'GENERAL' : existantRoom._id; - Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); - } else { - const users = channel.members - .reduce((ret, member) => { - if (member !== channel.creator) { - const user = this.getRocketUser(member); - if (user && user.username) { - ret.push(user.username); - } - } - return ret; - }, []); - let userId = startedByUserId; - this.users.users.forEach((user) => { - if (user.id === channel.creator && user.do_import) { - userId = user.rocketId; - } - }); - Meteor.runAsUser(userId, () => { - const returned = Meteor.call('createChannel', channel.name, users); - channel.rocketId = returned.rid; - }); - - // @TODO implement model specific function - const roomUpdate = { - ts: new Date(channel.created * 1000), - }; - if (!_.isEmpty(channel.topic && channel.topic.value)) { - roomUpdate.topic = channel.topic.value; - } - if (!_.isEmpty(channel.purpose && channel.purpose.value)) { - roomUpdate.description = channel.purpose.value; - } - Rooms.update({ _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } }); - } - this.addCountCompleted(1); - }); - }); - this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } }); + startImport(importSelection) { + const bots = this.collection.findOne({ import: this.importRecord._id, type: 'bots' }); + if (bots) { + this.bots = bots.bots || {}; + } else { + this.bots = {}; + } - const missedTypes = {}; + this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' }); + this.channels = RawImports.findOne({ import: this.importRecord._id, type: 'channels' }); + this.groups = RawImports.findOne({ import: this.importRecord._id, type: 'groups' }); + this.dms = RawImports.findOne({ import: this.importRecord._id, type: 'DMs' }); + this.mpims = RawImports.findOne({ import: this.importRecord._id, type: 'mpims' }); - super.updateProgress(ProgressStep.IMPORTING_MESSAGES); - Object.keys(this.messages).forEach((channel) => { - const messagesObj = this.messages[channel]; + this._userDataCache = {}; + this._anyExistingSlackMessage = Boolean(Messages.findOne({ _id: /slack\-.*/ })); + this.reloadCount(); - Meteor.runAsUser(startedByUserId, () => { - const slackChannel = this.getSlackChannelFromName(channel); - if (!slackChannel || !slackChannel.do_import) { - return; - } + super.startImport(importSelection); + const start = Date.now(); - const room = Rooms.findOneById(slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); - Object.keys(messagesObj).forEach((date) => { - const msgs = messagesObj[date]; - msgs.messages.forEach((message) => { - this.updateRecord({ messagesstatus: `${ channel }/${ date }.${ msgs.messages.length }` }); - return this.performMessageImport(message, room, missedTypes, slackChannel); - }); - }); - }); - }); + this._applyUserSelection(importSelection); + this._applyChannelSelection(importSelection); - if (!_.isEmpty(missedTypes)) { - console.log('Missed import types:', missedTypes); - } + const channelNames = []; + + const startedByUserId = Meteor.userId(); + Meteor.defer(() => { + try { + this._importUsers(startedByUserId); + + this._importChannels(startedByUserId, channelNames); + this._importGroups(startedByUserId, channelNames); + this._importMpims(startedByUserId, channelNames); + + this._importDMs(startedByUserId, channelNames); + + this._importMessages(startedByUserId, channelNames); super.updateProgress(ProgressStep.FINISHING); - this.channels.channels.forEach((channel) => { - if (channel.do_import && channel.is_archived) { - Meteor.runAsUser(startedByUserId, function() { - Meteor.call('archiveRoom', channel.rocketId); - }); + try { + if (this.channels) { + this._archiveChannelsAsNeeded(startedByUserId, this.channels); } - }); + if (this.groups) { + this._archiveChannelsAsNeeded(startedByUserId, this.groups); + } + if (this.mpims) { + this._archiveChannelsAsNeeded(startedByUserId, this.mpims); + } + } catch (e) { + // If it failed to archive some channel, it's no reason to flag the import as incomplete + // Just report the error but keep the import as successful. + console.error(e); + } super.updateProgress(ProgressStep.DONE); this.logger.log(`Import took ${ Date.now() - start } milliseconds.`); @@ -510,20 +797,94 @@ export class SlackImporter extends Base { this.logger.error(e); super.updateProgress(ProgressStep.ERROR); } + + this._userIdReference = {}; }); return this.getProgress(); } + _archiveChannelsAsNeeded(startedByUserId, list) { + list.channels.forEach((channel) => { + if (channel.do_import && channel.is_archived && channel.rocketId) { + Meteor.runAsUser(startedByUserId, function() { + Meteor.call('archiveRoom', channel.rocketId); + }); + } + }); + } + getSlackChannelFromName(channelName) { - return this.channels.channels.find((channel) => channel.name === channelName); + if (this.channels && this.channels.channels) { + const channel = this.channels.channels.find((channel) => channel.name === channelName); + if (channel) { + return channel; + } + } + + if (this.groups && this.groups.channels) { + const group = this.groups.channels.find((channel) => channel.name === channelName); + if (group) { + return group; + } + } + + if (this.mpims && this.mpims.channels) { + const group = this.mpims.channels.find((channel) => channel.name === channelName); + if (group) { + return group; + } + } + + if (this.dms && this.dms.channels) { + const dm = this.dms.channels.find((channel) => channel.id === channelName); + if (dm) { + return dm; + } + } + } + + _getBasicUserData(userId) { + if (this._userDataCache[userId]) { + return this._userDataCache[userId]; + } + + this._userDataCache[userId] = Users.findOneById(userId, { fields: { username: 1, name: 1 } }); + return this._userDataCache[userId]; } - getRocketUser(slackId) { - const user = this.users.users.find((user) => user.id === slackId); + getRocketUserFromUserId(userId) { + if (userId === 'rocket.cat' || userId === 'USLACKBOT') { + return this._getBasicUserData('rocket.cat'); + } - if (user) { - return Users.findOneById(user.rocketId, { fields: { username: 1, name: 1 } }); + const rocketId = this._getUserRocketId(userId); + if (rocketId) { + return this._getBasicUserData(rocketId); + } + + if (userId in this.bots) { + return this._getBasicUserData('rocket.cat'); + } + } + + getImportedRocketUserIdFromSlackUserId(slackUserId) { + if (slackUserId.toUpperCase() === 'USLACKBOT') { + return 'rocket.cat'; + } + + for (const user of this.users.users) { + if (user.id !== slackUserId) { + continue; + } + + if (user.do_import) { + return user.rocketId; + } + + if (user.is_bot) { + return 'rocket.cat'; + } } } @@ -539,6 +900,7 @@ export class SlackImporter extends Base { message = message.replace(/:memo:/g, ':pencil:'); message = message.replace(/:piggy:/g, ':pig:'); message = message.replace(/:uk:/g, ':gb:'); + message = message.replace(/<(http[s]?:[^|]*)\|([^>]*)>/g, '[$2]($1)'); message = message.replace(/<(http[s]?:[^>]*)>/g, '$1'); for (const userReplace of Array.from(this.userTags)) { @@ -552,9 +914,30 @@ export class SlackImporter extends Base { return message; } - getSelection() { - const selectionUsers = this.users.users.map((user) => new SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); - const selectionChannels = this.channels.channels.map((channel) => new SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); - return new Selection(this.name, selectionUsers, selectionChannels, this.importRecord.count.messages); + convertSlackFileToPendingFile(file) { + return { + downloadUrl: file.url_private_download, + id: file.id, + size: file.size, + name: file.name, + external: file.is_external, + source: 'slack', + original: { + ...file, + }, + }; + } + + convertMessageAttachments(attachments) { + if (!attachments || !attachments.length) { + return attachments; + } + + return attachments.map((attachment) => ({ + ...attachment, + text: this.convertSlackMessageToRocketChat(attachment.text), + title: this.convertSlackMessageToRocketChat(attachment.title), + fallback: this.convertSlackMessageToRocketChat(attachment.fallback), + })); } } diff --git a/app/importer/client/admin/adminImport.html b/app/importer/client/admin/adminImport.html index 009895c572b57..87fc49ab06d65 100644 --- a/app/importer/client/admin/adminImport.html +++ b/app/importer/client/admin/adminImport.html @@ -2,36 +2,53 @@
{{> header sectionName="Import" fullpage="true"}}
- {{#unless isAdmin}} + {{#unless hasPermission 'run-import'}}

{{_ "You_are_not_authorized_to_view_this_page"}}

{{else}} + {{#if isPreparing}} + {{> loading}} + {{else}} -
-

{{_ "Import History" }}

+
+ + {{#if anySuccessfulSlackImports}} + + {{/if}} +
-
-
{{_ "View the status of all import operations." }}
-
- + {{#if canShowCurrentOperation}} +
+

{{_ "Current_Import_Operation"}}

+
-
+
+ {{> importOperationSummary operation}} + + {{#if canContinueOperation}} + + {{else}} + {{#if canCheckOperationProgress}} + + {{/if}} + {{/if}} +
+ {{/if}} -
+ {{#if history}} +
+

{{_ "Recent_Import_History"}}

+
-
-
- {{#each importers}} -
-

{{_ name }}

-
-
{{getDescription .}}
-
- + {{#each history}} + {{#if isNotCurrentOperation}} +
+ {{> importOperationSummary .}}
-
+ {{/if}} {{/each}} -
-
+ {{/if}} + {{/if}} + {{/unless}}
diff --git a/app/importer/client/admin/adminImport.js b/app/importer/client/admin/adminImport.js index 09956ade17bbd..ddbf8fc0f8d63 100644 --- a/app/importer/client/admin/adminImport.js +++ b/app/importer/client/admin/adminImport.js @@ -1,45 +1,131 @@ -import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { ReactiveVar } from 'meteor/reactive-var'; +import toastr from 'toastr'; -import { hasRole } from '../../../authorization'; -import { t, handleError } from '../../../utils'; +import { t, APIClient } from '../../../utils'; import { SideNav } from '../../../ui-utils/client'; +import { ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ProgressStep } from '../../lib/ImporterProgressStep'; -import { Importers } from '..'; +import './adminImport.html'; +import './importOperationSummary.js'; Template.adminImport.helpers({ - isAdmin() { - return hasRole(Meteor.userId(), 'admin'); + isPreparing() { + return Template.instance().preparing.get(); }, - getDescription(importer) { - return TAPi18n.__('Importer_From_Description', { from: t(importer.name) }); + history() { + return Template.instance().history.get(); }, - importers() { - return Importers.getAll(); + + operation() { + return Template.instance().operation.get(); + }, + + isNotCurrentOperation() { + const operation = Template.instance().operation.get(); + if (!operation) { + return true; + } + + return operation._id !== this._id || !operation.valid; + }, + + canShowCurrentOperation() { + const operation = Template.instance().operation.get(); + return operation && operation.valid; + }, + + canContinueOperation() { + const operation = Template.instance().operation.get(); + if (!operation || !operation.valid) { + return false; + } + + const possibleStatus = [ProgressStep.USER_SELECTION].concat(ImportWaitingStates).concat(ImportFileReadyStates).concat(ImportPreparingStartedStates); + return possibleStatus.includes(operation.status); + }, + + canCheckOperationProgress() { + const operation = Template.instance().operation.get(); + if (!operation || !operation.valid) { + return false; + } + + return ImportingStartedStates.includes(operation.status); + }, + + anySuccessfulSlackImports() { + const history = Template.instance().history.get(); + if (!history) { + return false; + } + + for (const op of history) { + if (op.importerKey === 'slack' && op.status === ProgressStep.DONE) { + return true; + } + } + + return false; }, }); Template.adminImport.events({ - 'click .import-history'() { - FlowRouter.go('/admin/import/history'); + 'click .new-import-btn'() { + FlowRouter.go('/admin/import/new'); + }, + 'click .download-slack-files-btn'(event, template) { + template.preparing.set(true); + APIClient.post('v1/downloadPendingFiles').then((data) => { + template.preparing.set(false); + if (data.count) { + toastr.success(t('File_Downloads_Started')); + FlowRouter.go('/admin/import/progress'); + } else { + toastr.success(t('No_files_left_to_download')); + } + }).catch((error) => { + template.preparing.set(false); + if (error) { + console.error(error); + toastr.error(t('Failed_To_Download_Files')); + } + }); }, + 'click .prepare-btn'() { + FlowRouter.go('/admin/import/prepare'); + }, + 'click .progress-btn'() { + FlowRouter.go('/admin/import/progress'); + }, +}); + +Template.adminImport.onCreated(function() { + const instance = this; + this.preparing = new ReactiveVar(true); + this.history = new ReactiveVar([]); + this.operation = new ReactiveVar(false); - 'click .start-import'() { - const importer = this; + APIClient.get('v1/getCurrentImportOperation').then((data) => { + instance.operation.set(data.operation); - Meteor.call('setupImporter', importer.key, function(error) { + APIClient.get('v1/getLatestImportOperations').then((data) => { + instance.history.set(data); + instance.preparing.set(false); + }).catch((error) => { if (error) { - console.log(t('importer_setup_error'), importer.key, error); - handleError(error); - return; + toastr.error(t('Failed_To_Load_Import_History')); + instance.preparing.set(false); } - - FlowRouter.go(`/admin/import/prepare/${ importer.key }`); }); - }, + }).catch((error) => { + if (error) { + toastr.error(t('Failed_To_Load_Import_Operation')); + instance.preparing.set(false); + } + }); }); Template.adminImport.onRendered(() => { diff --git a/app/importer/client/admin/adminImportHistory.html b/app/importer/client/admin/adminImportHistory.html deleted file mode 100644 index 4dc635dc11754..0000000000000 --- a/app/importer/client/admin/adminImportHistory.html +++ /dev/null @@ -1,135 +0,0 @@ - diff --git a/app/importer/client/admin/adminImportHistory.js b/app/importer/client/admin/adminImportHistory.js deleted file mode 100644 index 2fb1ccf928c42..0000000000000 --- a/app/importer/client/admin/adminImportHistory.js +++ /dev/null @@ -1,205 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { Template } from 'meteor/templating'; -import toastr from 'toastr'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { hasRole } from '../../../authorization'; -import { t, APIClient } from '../../../utils'; -import { ProgressStep } from '../../lib/ImporterProgressStep'; -import { SideNav } from '../../../ui-utils/client'; - -import { Importers } from '..'; - -Template.adminImportHistory.helpers({ - isAdmin() { - return hasRole(Meteor.userId(), 'admin'); - }, - importers() { - return Importers.getAll(); - }, - isPreparing() { - return Template.instance().preparing.get(); - }, - history() { - return Template.instance().history.get(); - }, - statusMessage() { - const statusKey = this.status || ProgressStep.NEW; - - switch (statusKey) { - case ProgressStep.NEW: - return t('Not_started'); - case ProgressStep.UPLOADING: - return t('Uploading_file'); - case ProgressStep.DOWNLOADING_FILE_URL: - return t('Downloading_file_from_external_URL'); - case ProgressStep.DOWNLOAD_COMPLETE: - return t('Successfully_downloaded_file_from_external_URL_should_start_preparing_soon'); - case ProgressStep.PREPARING_STARTED: - return t('Preparing_data_for_import_process'); - case ProgressStep.PREPARING_USERS: - return t('Preparing_list_of_users'); - case ProgressStep.PREPARING_CHANNELS: - return t('Preparing_list_of_channels'); - case ProgressStep.PREPARING_MESSAGES: - return t('Preparing_list_of_messages'); - case ProgressStep.USER_SELECTION: - return t('Selecting_users'); - case ProgressStep.IMPORTING_STARTED: - return t('Started'); - case ProgressStep.IMPORTING_USERS: - return t('Importing_users'); - case ProgressStep.IMPORTING_CHANNELS: - return t('Importing_channels'); - case ProgressStep.IMPORTING_MESSAGES: - return t('Importing_messages'); - case ProgressStep.FINISHING: - return t('Almost_done'); - case ProgressStep.DONE: - return t('Completed'); - case ProgressStep.ERROR: - return t('Error'); - case ProgressStep.CANCELLED: - return t('Canceled'); - default: - return statusKey; - } - }, - - lastUpdated() { - if (!this._updatedAt) { - return ''; - } - - const date = new Date(this._updatedAt); - return date.toLocaleString(); - }, - - hasCounters() { - return Boolean(this.count); - }, - - userCount() { - if (this.count && this.count.users) { - return this.count.users; - } - - return 0; - }, - - channelCount() { - if (this.count && this.count.channels) { - return this.count.channels; - } - - return 0; - }, - - messageCount() { - if (this.count && this.count.messages) { - return this.count.messages; - } - - return 0; - }, - - totalCount() { - if (this.count && this.count.total) { - return this.count.total; - } - - return 0; - }, - - hasErrors() { - if (!this.fileData) { - return false; - } - - if (this.fileData.users) { - for (const user of this.fileData.users) { - if (user.is_email_taken) { - return true; - } - if (user.error) { - return true; - } - } - } - - if (this.errors && this.errors.length > 0) { - return true; - } - - return false; - }, - - formatedError() { - if (!this.error) { - return ''; - } - - if (typeof this.error === 'string') { - return this.error; - } - - if (typeof this.error === 'object') { - if (this.error.message) { - return this.error.message; - } - if (this.error.error && typeof this.error.error === 'string') { - return this.error.error; - } - - try { - const json = JSON.stringify(this.error); - console.log(json); - return json; - } catch (e) { - return t('Error'); - } - } - - return this.error.toString(); - }, - - messageTime() { - if (!this.msg || !this.msg.ts) { - return ''; - } - - const date = new Date(this.msg.ts); - return date.toLocaleString(); - }, -}); - -Template.adminImportHistory.events({ - 'click .import-list'() { - FlowRouter.go('/admin/import'); - }, -}); - -Template.adminImportHistory.onCreated(function() { - const instance = this; - this.preparing = new ReactiveVar(true); - this.history = new ReactiveVar([]); - - APIClient.get('v1/getLatestImportOperations').then((data) => { - instance.history.set(data); - instance.preparing.set(false); - }).catch((error) => { - if (error) { - toastr.error(t('Failed_To_Load_Import_Data')); - instance.preparing.set(false); - } - }); -}); - -Template.adminImportHistory.onRendered(() => { - Tracker.afterFlush(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }); -}); diff --git a/app/importer/client/admin/adminImportNew.html b/app/importer/client/admin/adminImportNew.html new file mode 100644 index 0000000000000..17e9230b5cb24 --- /dev/null +++ b/app/importer/client/admin/adminImportNew.html @@ -0,0 +1,83 @@ + diff --git a/app/importer/client/admin/adminImportNew.js b/app/importer/client/admin/adminImportNew.js new file mode 100644 index 0000000000000..2d643312dc1c2 --- /dev/null +++ b/app/importer/client/admin/adminImportNew.js @@ -0,0 +1,158 @@ +import { Tracker } from 'meteor/tracker'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { ReactiveVar } from 'meteor/reactive-var'; +import toastr from 'toastr'; + +import { t, APIClient } from '../../../utils'; +import { SideNav } from '../../../ui-utils/client'; +import { settings } from '../../../settings'; +import { showImporterException } from '../functions/showImporterException'; + +import { Importers } from '..'; + +import './adminImportNew.html'; + +Template.adminImportNew.helpers({ + isPreparing() { + return Template.instance().preparing.get(); + }, + importers() { + return Importers.getAll(); + }, + pageTitle() { + const importerKey = Template.instance().importType.get(); + if (!importerKey) { + return t('Import_New_File'); + } + + const importer = Importers.get(importerKey); + if (!importer) { + return t('Import_New_File'); + } + + return TAPi18n.__('Importer_From_Description', { from: t(importer.name) }); + }, + importType() { + return Template.instance().importType.get(); + }, + fileType() { + return Template.instance().fileType.get(); + }, + isImporterSelected() { + return Template.instance().importType.get(); + }, + isFileTypeSelected() { + return Template.instance().fileType.get(); + }, + isUpload() { + return Template.instance().fileType.get() === 'upload'; + }, + isPublicURL() { + return Template.instance().fileType.get() === 'url'; + }, + isServerFile() { + return Template.instance().fileType.get() === 'path'; + }, + fileSizeLimitMessage() { + const maxFileSize = settings.get('FileUpload_MaxFileSize'); + let message; + + if (maxFileSize > 0) { + const sizeInKb = maxFileSize / 1024; + const sizeInMb = sizeInKb / 1024; + + let fileSizeMessage; + if (sizeInMb > 0) { + fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) }); + } else if (sizeInKb > 0) { + fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) }); + } else { + fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) }); + } + + message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage }); + } else { + message = TAPi18n.__('Importer_Upload_Unlimited_FileSize'); + } + + return message; + }, +}); + +Template.adminImportNew.events({ + 'change .file-type'(event, template) { + template.fileType.set($('select[name=file-type]').val()); + }, + 'change .import-type'(event, template) { + template.importType.set($('select[name=import-type]').val()); + }, + + 'change .import-file-input'(event, template) { + const importType = template.importType.get(); + + const e = event.originalEvent || event; + let { files } = e.target; + if (!files || (files.length === 0)) { + files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || []; + } + + Array.from(files).forEach((file) => { + template.preparing.set(true); + + const reader = new FileReader(); + + reader.readAsDataURL(file); + reader.onloadend = () => { + APIClient.post('v1/uploadImportFile', { + binaryContent: reader.result.split(';base64,')[1], + contentType: file.type, + fileName: file.name, + importerKey: importType, + }).then(() => { + toastr.success(t('File_uploaded_successfully')); + FlowRouter.go('/admin/import/prepare'); + }).catch((error) => { + if (error) { + showImporterException(error); + template.preparing.set(false); + } + }); + }; + }); + }, + + 'click .import-btn'(event, template) { + const importType = template.importType.get(); + const fileUrl = $('.import-file-url').val(); + + template.preparing.set(true); + + APIClient.post('v1/downloadPublicImportFile', { + fileUrl, + importerKey: importType, + }).then(() => { + toastr.success(t('Import_requested_successfully')); + FlowRouter.go('/admin/import/prepare'); + }).catch((error) => { + if (error) { + showImporterException(error); + template.preparing.set(false); + } + }); + }, +}); + +Template.adminImportNew.onCreated(function() { + this.preparing = new ReactiveVar(false); + this.importType = new ReactiveVar(''); + this.fileType = new ReactiveVar('upload'); +}); + +Template.adminImportNew.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/importer/client/admin/adminImportPrepare.html b/app/importer/client/admin/adminImportPrepare.html index 2d817c635382c..54f4fc2369f4d 100644 --- a/app/importer/client/admin/adminImportPrepare.html +++ b/app/importer/client/admin/adminImportPrepare.html @@ -1,100 +1,71 @@ diff --git a/app/importer/client/admin/adminImportPrepare.js b/app/importer/client/admin/adminImportPrepare.js index 479a791f9b275..f37dc66a4b803 100644 --- a/app/importer/client/admin/adminImportPrepare.js +++ b/app/importer/client/admin/adminImportPrepare.js @@ -1,32 +1,35 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { ReactiveVar } from 'meteor/reactive-var'; import toastr from 'toastr'; -import { hasRole } from '../../../authorization'; -import { settings } from '../../../settings'; -import { t, handleError, APIClient } from '../../../utils'; +import { t, APIClient } from '../../../utils'; import { SideNav } from '../../../ui-utils/client'; +import { ProgressStep, ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ImportingErrorStates } from '../../lib/ImporterProgressStep'; +import { showImporterException } from '../functions/showImporterException'; -import { Importers } from '..'; +import { ImporterWebsocketReceiver } from '..'; + +import './adminImportPrepare.html'; Template.adminImportPrepare.helpers({ - isAdmin() { - return hasRole(Meteor.userId(), 'admin'); + isPreparing() { + return Template.instance().preparing.get(); + }, + hasProgressRate() { + return Template.instance().progressRate.get() !== false; }, - importer() { - const importerKey = FlowRouter.getParam('importer'); + progressRate() { + const rate = Template.instance().progressRate.get(); + if (rate) { + return `${ rate }%`; + } - return Importers.get(importerKey); + return ''; }, - isLoaded() { - return Template.instance().loaded.get(); - }, - isPreparing() { - return Template.instance().preparing.get(); + pageTitle() { + return t('Importing_Data'); }, users() { return Template.instance().users.get(); @@ -37,282 +40,154 @@ Template.adminImportPrepare.helpers({ message_count() { return Template.instance().message_count.get(); }, - fileSizeLimitMessage() { - const maxFileSize = settings.get('FileUpload_MaxFileSize'); - let message; - - if (maxFileSize > 0) { - const sizeInKb = maxFileSize / 1024; - const sizeInMb = sizeInKb / 1024; - - let fileSizeMessage; - if (sizeInMb > 0) { - fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) }); - } else if (sizeInKb > 0) { - fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) }); - } else { - fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) }); - } - - message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage }); - } else { - message = TAPi18n.__('Importer_Upload_Unlimited_FileSize'); - } - - return message; - }, }); -function showToastrForErrorType(errorType, defaultErrorString) { - let errorCode; - - if (typeof errorType === 'string') { - errorCode = errorType; - } else if (typeof errorType === 'object') { - if (errorType.error && typeof errorType.error === 'string') { - errorCode = errorType.error; +Template.adminImportPrepare.events({ + 'click .button.start'(event, template) { + const btn = this; + $(btn).prop('disabled', true); + for (const user of Array.from(template.users.get())) { + user.do_import = $(`[name='${ user.user_id }']`).is(':checked'); } - } - if (errorCode) { - const errorTranslation = t(errorCode); - if (errorTranslation !== errorCode) { - toastr.error(errorTranslation); - return; + for (const channel of Array.from(template.channels.get())) { + channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked'); } - } - toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); -} + APIClient.post('v1/startImport', { input: { users: template.users.get(), channels: template.channels.get() } }).then(() => { + template.users.set([]); + template.channels.set([]); + return FlowRouter.go('/admin/import/progress'); + }).catch((error) => { + if (error) { + showImporterException(error, 'Failed_To_Start_Import'); + return FlowRouter.go('/admin/import'); + } + }); + }, -function showException(error, defaultErrorString) { - console.error(error); - if (error && error.xhr && error.xhr.responseJSON) { - console.log(error.xhr.responseJSON); + 'click .button.uncheck-deleted-users'(event, template) { + Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => { + const box = $(`[name=${ user.user_id }]`); + return box && box.length && box[0].checked && box.click(); + }); + }, - if (error.xhr.responseJSON.errorType) { - showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString); - return; - } - } + 'click .button.uncheck-archived-channels'(event, template) { + Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => { + const box = $(`[name=${ channel.channel_id }]`); + return box && box.length && box[0].checked && box.click(); + }); + }, - toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); -} +}); -function getImportFileData(importer, template) { - APIClient.get(`v1/getImportFileData?importerKey=${ importer.key }`).then((data) => { +function getImportFileData(template) { + APIClient.get('v1/getImportFileData').then((data) => { if (!data) { - console.warn(`The importer ${ importer.key } is not set up correctly, as it did not return any data.`); + console.warn('The importer is not set up correctly, as it did not return any data.'); toastr.error(t('Importer_not_setup')); - template.preparing.set(false); - return; + return FlowRouter.go('/admin/import'); } if (data.waiting) { setTimeout(() => { - getImportFileData(importer, template); + getImportFileData(template); }, 1000); return; } if (data.step) { console.warn('Invalid file, contains `data.step`.', data); - toastr.error(t('Invalid_Export_File', importer.key)); - template.preparing.set(false); - return; + toastr.error(t('Failed_To_Load_Import_Data')); + return FlowRouter.go('/admin/import'); } template.users.set(data.users); template.channels.set(data.channels); template.message_count.set(data.message_count); - template.loaded.set(true); template.preparing.set(false); + template.progressRate.set(false); }).catch((error) => { if (error) { - showException(error, 'Failed_To_Load_Import_Data'); - template.preparing.set(false); + showImporterException(error, 'Failed_To_Load_Import_Data'); + return FlowRouter.go('/admin/import'); } }); } -Template.adminImportPrepare.events({ - 'change .import-file-input'(event, template) { - const importer = this; - if (!importer || !importer.key) { return; } +function loadOperation(template) { + APIClient.get('v1/getCurrentImportOperation').then((data) => { + const { operation } = data; - const e = event.originalEvent || event; - let { files } = e.target; - if (!files || (files.length === 0)) { - files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || []; + if (!operation.valid) { + return FlowRouter.go('/admin/import/new'); } - Array.from(files).forEach((file) => { - template.preparing.set(true); - - const reader = new FileReader(); - - reader.readAsBinaryString(file); - reader.onloadend = () => { - APIClient.post('v1/uploadImportFile', { - binaryContent: reader.result, - contentType: file.type, - fileName: file.name, - importerKey: importer.key, - }).then(() => { - getImportFileData(importer, template); - }).catch((error) => { - if (error) { - showException(error); - template.preparing.set(false); - } - }); - }; - }); - }, - - - 'click .download-public-url'(event, template) { - const importer = this; - if (!importer || !importer.key) { return; } - - const fileUrl = $('.import-file-url').val(); + // If the import has already started, move to the progress screen + if (ImportingStartedStates.includes(operation.status)) { + return FlowRouter.go('/admin/import/progress'); + } - template.preparing.set(true); + // The getImportFileData method can handle it if the state is: + // 1) ready to select the users, + // 2) preparing + // 3) ready to be prepared - APIClient.post('v1/downloadPublicImportFile', { - fileUrl, - importerKey: importer.key, - }).then(() => { - getImportFileData(importer, template); - }).catch((error) => { - if (error) { - showException(error); - template.preparing.set(false); + if (operation.status === ProgressStep.USER_SELECTION || ImportPreparingStartedStates.includes(operation.status) || ImportFileReadyStates.includes(operation.status)) { + if (!template.callbackRegistered) { + ImporterWebsocketReceiver.registerCallback(template.progressUpdated); + template.callbackRegistered = true; } - }); - }, - - 'click .button.start'(event, template) { - const btn = this; - $(btn).prop('disabled', true); - for (const user of Array.from(template.users.get())) { - user.do_import = $(`[name='${ user.user_id }']`).is(':checked'); + getImportFileData(template); + return template.preparing.set(true); } - for (const channel of Array.from(template.channels.get())) { - channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked'); - } - - Meteor.call('startImport', FlowRouter.getParam('importer'), { users: template.users.get(), channels: template.channels.get() }, function(error) { - if (error) { - console.warn('Error on starting the import:', error); - handleError(error); - } else { - FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`); - } - }); - }, + // We're still waiting for a file... This shouldn't take long + if (ImportWaitingStates.includes(operation.status)) { + setTimeout(() => { + loadOperation(template); + }, 1000); - 'click .button.restart'(event, template) { - Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error) { - if (error) { - console.warn('Error while restarting the import:', error); - handleError(error); - return; - } + return template.preparing.set(true); + } - template.users.set([]); - template.channels.set([]); - template.loaded.set(false); - }); - }, + if (ImportingErrorStates.includes(operation.status)) { + toastr.error(t('Import_Operation_Failed')); + return FlowRouter.go('/admin/import'); + } - 'click .button.uncheck-deleted-users'(event, template) { - Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => { - const box = $(`[name=${ user.user_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, + if (operation.status === ProgressStep.DONE) { + return FlowRouter.go('/admin/import'); + } - 'click .button.uncheck-archived-channels'(event, template) { - Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => { - const box = $(`[name=${ channel.channel_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, -}); + toastr.error(t('Unknown_Import_State')); + return FlowRouter.go('/admin/import'); + }).catch((error) => { + if (error) { + toastr.error(t('Failed_To_Load_Import_Data')); + return FlowRouter.go('/admin/import'); + } + }); +} Template.adminImportPrepare.onCreated(function() { - const instance = this; this.preparing = new ReactiveVar(true); - this.loaded = new ReactiveVar(false); + this.progressRate = new ReactiveVar(false); + this.callbackRegistered = false; this.users = new ReactiveVar([]); this.channels = new ReactiveVar([]); this.message_count = new ReactiveVar(0); - function loadSelection(progress) { - if (progress != null ? progress.step : undefined) { - switch (progress.step) { - // When the import is running, take the user to the progress page - case 'importer_importing_started': - case 'importer_importing_users': - case 'importer_importing_channels': - case 'importer_importing_messages': - case 'importer_finishing': - return FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`); - case 'importer_user_selection': - return Meteor.call('getSelectionData', FlowRouter.getParam('importer'), function(error, data) { - if (error) { - handleError(error); - } - instance.users.set(data.users); - instance.channels.set(data.channels); - instance.message_count.set(data.message_count); - instance.loaded.set(true); - return instance.preparing.set(false); - }); - case 'importer_new': - return instance.preparing.set(false); - default: - return Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error, progress) { - if (error) { - handleError(error); - } - return loadSelection(progress); - }); - } - } else { - return console.warn('Invalid progress information.', progress); + this.progressUpdated = (progress) => { + if ('rate' in progress) { + const { rate } = progress; + this.progressRate.set(rate); } - } + }; - // Load the initial progress to determine what we need to do - if (FlowRouter.getParam('importer')) { - return Meteor.call('getImportProgress', FlowRouter.getParam('importer'), function(error, progress) { - if (error) { - console.warn('Error while getting the import progress:', error); - handleError(error); - return; - } - - // if the progress isnt defined, that means there currently isn't an instance - // of the importer, so we need to create it - if (progress === undefined) { - return Meteor.call('setupImporter', FlowRouter.getParam('importer'), function(err, data) { - if (err) { - handleError(err); - } - instance.preparing.set(false); - return loadSelection(data); - }); - } - // Otherwise, we might need to do something based upon the current step - // of the import - return loadSelection(progress); - }); - } - return FlowRouter.go('/admin/import'); + loadOperation(this); }); Template.adminImportPrepare.onRendered(() => { @@ -321,3 +196,8 @@ Template.adminImportPrepare.onRendered(() => { SideNav.openFlex(); }); }); + +Template.adminImportPrepare.onDestroyed(function() { + this.callbackRegistered = false; + ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated); +}); diff --git a/app/importer/client/admin/adminImportProgress.html b/app/importer/client/admin/adminImportProgress.html index 9acfb08a8ec12..05337b38b150a 100644 --- a/app/importer/client/admin/adminImportProgress.html +++ b/app/importer/client/admin/adminImportProgress.html @@ -2,6 +2,7 @@ {{> loading}}

{{step}}

{{completed}} / {{total}}

+

{{progressRate}}

{{_ "You_can_close_this_window_now"}}

diff --git a/app/importer/client/admin/adminImportProgress.js b/app/importer/client/admin/adminImportProgress.js index bb7f5f930e03b..cf59ab3c79cc9 100644 --- a/app/importer/client/admin/adminImportProgress.js +++ b/app/importer/client/admin/adminImportProgress.js @@ -1,12 +1,14 @@ -import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; import toastr from 'toastr'; -import { t, handleError } from '../../../utils'; +import { t, handleError, APIClient } from '../../../utils'; +import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep'; -import { Importers, ImporterWebsocketReceiver, ProgressStep } from '..'; +import { ImporterWebsocketReceiver } from '..'; + +import './adminImportProgress.html'; Template.adminImportProgress.helpers({ step() { @@ -18,20 +20,33 @@ Template.adminImportProgress.helpers({ total() { return Template.instance().total.get(); }, + progressRate() { + try { + const instance = Template.instance(); + const completed = instance.completed.get(); + const total = instance.total.get(); + + const rate = Math.floor(completed * 10000 / total) / 100; + + if (isNaN(rate)) { + return ''; + } + + return `${ rate }%`; + } catch { + return ''; + } + }, }); Template.adminImportProgress.onCreated(function() { - const instance = this; + const template = this; + this.operation = new ReactiveVar(false); this.step = new ReactiveVar(t('Loading...')); this.completed = new ReactiveVar(0); this.total = new ReactiveVar(0); - // Ensure there is an importer how they're accessing it - const key = FlowRouter.getParam('importer').toLowerCase(); - if (key === '' || !Importers.get(key)) { - FlowRouter.go('/admin/import'); - return; - } + let importerKey = false; function _updateProgress(progress) { switch (progress.step) { @@ -39,46 +54,84 @@ Template.adminImportProgress.onCreated(function() { toastr.success(t(progress.step[0].toUpperCase() + progress.step.slice(1))); return FlowRouter.go('/admin/import'); case ProgressStep.ERROR: + case ProgressStep.CANCELLED: toastr.error(t(progress.step[0].toUpperCase() + progress.step.slice(1))); - return FlowRouter.go(`/admin/import/prepare/${ key }`); + return FlowRouter.go('/admin/import'); default: - instance.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1))); - instance.completed.set(progress.count.completed); - instance.total.set(progress.count.total); + template.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1))); + if (progress.count.completed) { + template.completed.set(progress.count.completed); + } + if (progress.count.total) { + template.total.set(progress.count.total); + } break; } } this.progressUpdated = function _progressUpdated(progress) { - if (progress.key.toLowerCase() !== key) { + if (progress.key.toLowerCase() !== importerKey) { return; } _updateProgress(progress); }; - Meteor.call('getImportProgress', key, function(error, progress) { - if (error) { - console.warn('Error on getting the import progress:', error); - handleError(error); + APIClient.get('v1/getCurrentImportOperation').then((data) => { + const { operation } = data; + + if (!operation.valid) { return FlowRouter.go('/admin/import'); } - if (!progress) { - toastr.warning(t('Importer_not_in_progress')); - return FlowRouter.go(`/admin/import/prepare/${ key }`); + // If the import has not started, move to the prepare screen + if (!ImportingStartedStates.includes(operation.status)) { + return FlowRouter.go('/admin/import/prepare'); + } + + importerKey = operation.importerKey; + template.operation.set(operation); + if (operation.count) { + if (operation.count.total) { + template.total.set(operation.count.total); + } + if (operation.count.completed) { + template.completed.set(operation.count.completed); + } } - const whereTo = _updateProgress(progress); + APIClient.get('v1/getImportProgress').then((progress) => { + if (!progress) { + toastr.warning(t('Importer_not_in_progress')); + return FlowRouter.go('/admin/import/prepare'); + } + + const whereTo = _updateProgress(progress); + + if (!whereTo) { + ImporterWebsocketReceiver.registerCallback(template.progressUpdated); + } + }).catch((error) => { + console.warn('Error on getting the import progress:', error); - if (!whereTo) { - ImporterWebsocketReceiver.registerCallback(instance.progressUpdated); + if (error) { + handleError(error); + } else { + toastr.error(t('Failed_To_Load_Import_Data')); + } + + return FlowRouter.go('/admin/import'); + }); + }).catch((error) => { + if (error) { + handleError(error); + } else { + toastr.error(t('Failed_To_Load_Import_Data')); } + return FlowRouter.go('/admin/import'); }); }); Template.adminImportProgress.onDestroyed(function() { - const instance = this; - - ImporterWebsocketReceiver.unregisterCallback(instance.progressUpdated); + ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated); }); diff --git a/app/importer/client/admin/importOperationSummary.html b/app/importer/client/admin/importOperationSummary.html new file mode 100644 index 0000000000000..2378a4ebddc80 --- /dev/null +++ b/app/importer/client/admin/importOperationSummary.html @@ -0,0 +1,115 @@ + diff --git a/app/importer/client/admin/importOperationSummary.js b/app/importer/client/admin/importOperationSummary.js new file mode 100644 index 0000000000000..0fb5293a044ad --- /dev/null +++ b/app/importer/client/admin/importOperationSummary.js @@ -0,0 +1,136 @@ +import { Template } from 'meteor/templating'; + +import { t } from '../../../utils'; + +import './importOperationSummary.html'; + +Template.importOperationSummary.helpers({ + lastUpdated() { + if (!this._updatedAt) { + return ''; + } + + const date = new Date(this._updatedAt); + return date.toLocaleString(); + }, + + status() { + if (!this.status) { + return ''; + } + + return t(this.status.replace('importer_', 'importer_status_')); + }, + + fileName() { + const fileName = this.file; + if (!fileName) { + return ''; + } + + // If the userid is inside the filename, remove it and anything before it + const idx = fileName.indexOf(`_${ this.user }_`); + if (idx >= 0) { + return fileName.substring(idx + this.user.length + 2); + } + + return fileName; + }, + + hasCounters() { + return Boolean(this.count); + }, + + userCount() { + if (this.count && this.count.users) { + return this.count.users; + } + + return 0; + }, + + channelCount() { + if (this.count && this.count.channels) { + return this.count.channels; + } + + return 0; + }, + + messageCount() { + if (this.count && this.count.messages) { + return this.count.messages; + } + + return 0; + }, + + totalCount() { + if (this.count && this.count.total) { + return this.count.total; + } + + return 0; + }, + + hasErrors() { + if (!this.fileData) { + return false; + } + + if (this.fileData.users) { + for (const user of this.fileData.users) { + if (user.is_email_taken) { + return true; + } + if (user.error) { + return true; + } + } + } + + if (this.errors && this.errors.length > 0) { + return true; + } + + return false; + }, + + formatedError() { + if (!this.error) { + return ''; + } + + if (typeof this.error === 'string') { + return this.error; + } + + if (typeof this.error === 'object') { + if (this.error.message) { + return this.error.message; + } + if (this.error.error && typeof this.error.error === 'string') { + return this.error.error; + } + + try { + const json = JSON.stringify(this.error); + console.log(json); + return json; + } catch (e) { + return t('Error'); + } + } + + return this.error.toString(); + }, + + messageTime() { + if (!this.msg || !this.msg.ts) { + return ''; + } + + const date = new Date(this.msg.ts); + return date.toLocaleString(); + }, +}); diff --git a/app/importer/client/admin/views.js b/app/importer/client/admin/views.js deleted file mode 100644 index 5e6c2fc3ed0e4..0000000000000 --- a/app/importer/client/admin/views.js +++ /dev/null @@ -1,8 +0,0 @@ -import './adminImport.html'; -import './adminImport'; -import './adminImportHistory.html'; -import './adminImportHistory'; -import './adminImportPrepare.html'; -import './adminImportPrepare'; -import './adminImportProgress.html'; -import './adminImportProgress'; diff --git a/app/importer/client/functions/showImporterException.js b/app/importer/client/functions/showImporterException.js new file mode 100644 index 0000000000000..4312e48148419 --- /dev/null +++ b/app/importer/client/functions/showImporterException.js @@ -0,0 +1,41 @@ +import toastr from 'toastr'; + +import { t } from '../../../utils'; + +function showToastrForErrorType(errorType, defaultErrorString) { + let errorCode; + + if (typeof errorType === 'string') { + errorCode = errorType; + } else if (typeof errorType === 'object') { + if (errorType.error && typeof errorType.error === 'string') { + errorCode = errorType.error; + } + } + + if (errorCode) { + const errorTranslation = t(errorCode); + if (errorTranslation !== errorCode) { + toastr.error(errorTranslation); + return; + } + } + + toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); +} + +function showException(error, defaultErrorString) { + console.error(error); + if (error && error.xhr && error.xhr.responseJSON) { + console.log(error.xhr.responseJSON); + + if (error.xhr.responseJSON.errorType) { + showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString); + return; + } + } + + toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); +} + +export const showImporterException = showException; diff --git a/app/importer/client/index.js b/app/importer/client/index.js index 68c66ec4b4eab..1ae15dfc877f7 100644 --- a/app/importer/client/index.js +++ b/app/importer/client/index.js @@ -6,35 +6,34 @@ import { Importers } from '../lib/Importers'; import { ImporterInfo } from '../lib/ImporterInfo'; import { ProgressStep } from '../lib/ImporterProgressStep'; - FlowRouter.route('/admin/import', { name: 'admin-import', async action() { - await import('./admin/views'); + await import('./admin/adminImport'); BlazeLayout.render('main', { center: 'adminImport' }); }, }); -FlowRouter.route('/admin/import/history', { - name: 'admin-import-history', +FlowRouter.route('/admin/import/new', { + name: 'admin-import-new', async action() { - await import('./admin/views'); - BlazeLayout.render('main', { center: 'adminImportHistory' }); + await import('./admin/adminImportNew'); + BlazeLayout.render('main', { center: 'adminImportNew' }); }, }); -FlowRouter.route('/admin/import/prepare/:importer', { +FlowRouter.route('/admin/import/prepare', { name: 'admin-import-prepare', async action() { - await import('./admin/views'); + await import('./admin/adminImportPrepare'); BlazeLayout.render('main', { center: 'adminImportPrepare' }); }, }); -FlowRouter.route('/admin/import/progress/:importer', { +FlowRouter.route('/admin/import/progress', { name: 'admin-import-progress', async action() { - await import('./admin/views'); + await import('./admin/adminImportProgress'); BlazeLayout.render('main', { center: 'adminImportProgress' }); }, }); diff --git a/app/importer/lib/ImporterProgressStep.js b/app/importer/lib/ImporterProgressStep.js index 8571ccb533fa9..e61db4e77f7f0 100644 --- a/app/importer/lib/ImporterProgressStep.js +++ b/app/importer/lib/ImporterProgressStep.js @@ -1,20 +1,62 @@ /** The progress step that an importer is at. */ export const ProgressStep = Object.freeze({ - UPLOADING: 'importer_uploading', NEW: 'importer_new', - DOWNLOADING_FILE_URL: 'downloading_file_url', - DOWNLOAD_COMPLETE: 'download_complete', + UPLOADING: 'importer_uploading', + DOWNLOADING_FILE: 'importer_downloading_file', + FILE_LOADED: 'importer_file_loaded', + PREPARING_STARTED: 'importer_preparing_started', PREPARING_USERS: 'importer_preparing_users', PREPARING_CHANNELS: 'importer_preparing_channels', PREPARING_MESSAGES: 'importer_preparing_messages', + USER_SELECTION: 'importer_user_selection', + IMPORTING_STARTED: 'importer_importing_started', IMPORTING_USERS: 'importer_importing_users', IMPORTING_CHANNELS: 'importer_importing_channels', IMPORTING_MESSAGES: 'importer_importing_messages', + IMPORTING_FILES: 'importer_importing_files', FINISHING: 'importer_finishing', + DONE: 'importer_done', ERROR: 'importer_import_failed', CANCELLED: 'importer_import_cancelled', }); + +export const ImportWaitingStates = [ + ProgressStep.NEW, + ProgressStep.UPLOADING, + ProgressStep.DOWNLOADING_FILE, +]; + +export const ImportFileReadyStates = [ + ProgressStep.FILE_LOADED, +]; + +export const ImportPreparingStartedStates = [ + ProgressStep.PREPARING_STARTED, + ProgressStep.PREPARING_USERS, + ProgressStep.PREPARING_CHANNELS, + ProgressStep.PREPARING_MESSAGES, +]; + +export const ImportingStartedStates = [ + ProgressStep.IMPORTING_STARTED, + ProgressStep.IMPORTING_USERS, + ProgressStep.IMPORTING_CHANNELS, + ProgressStep.IMPORTING_MESSAGES, + ProgressStep.IMPORTING_FILES, + ProgressStep.FINISHING, +]; + +export const ImportingDoneStates = [ + ProgressStep.DONE, + ProgressStep.ERROR, + ProgressStep.CANCELLED, +]; + +export const ImportingErrorStates = [ + ProgressStep.ERROR, + ProgressStep.CANCELLED, +]; diff --git a/app/importer/server/classes/ImporterBase.js b/app/importer/server/classes/ImporterBase.js index f7cebb0f885c4..d672c81d38de9 100644 --- a/app/importer/server/classes/ImporterBase.js +++ b/app/importer/server/classes/ImporterBase.js @@ -10,10 +10,9 @@ import { Progress } from './ImporterProgress'; import { Selection } from './ImporterSelection'; import { ImporterWebsocket } from './ImporterWebsocket'; import { ProgressStep } from '../../lib/ImporterProgressStep'; -import { Imports } from '../models/Imports'; import { ImporterInfo } from '../../lib/ImporterInfo'; import { RawImports } from '../models/RawImports'; -import { Settings } from '../../../models'; +import { Settings, Imports } from '../../../models'; import { Logger } from '../../../logger'; import { FileUpload } from '../../../file-upload'; import { sendMessage } from '../../../lib'; @@ -76,7 +75,7 @@ export class Base { * @param {string} description The i18n string which describes the importer * @param {string} mimeType The expected file type. */ - constructor(info) { + constructor(info, importRecord) { if (!(info instanceof ImporterInfo)) { throw new Error('Information passed in must be a valid ImporterInfo instance.'); } @@ -88,7 +87,6 @@ export class Base { this.prepare = this.prepare.bind(this); this.startImport = this.startImport.bind(this); - this.getSelection = this.getSelection.bind(this); this.getProgress = this.getProgress.bind(this); this.updateProgress = this.updateProgress.bind(this); this.addCountToTotal = this.addCountToTotal.bind(this); @@ -103,7 +101,6 @@ export class Base { this.collection = RawImports; const userId = Meteor.userId(); - const importRecord = Imports.findPendingImport(this.info.key); if (importRecord) { this.logger.debug('Found existing import operation'); @@ -202,15 +199,6 @@ export class Base { return this.updateProgress(ProgressStep.IMPORTING_STARTED); } - /** - * Gets the Selection object for the import. - * - * @returns {Selection} The users and channels selection - */ - getSelection() { - throw new Error(`Invalid 'getSelection' called on ${ this.info.name }, it must be overridden and super can not be called.`); - } - /** * Gets the progress of this import. * @@ -262,11 +250,21 @@ export class Base { this.logger.debug(`${ this.info.name } is now at ${ step }.`); this.updateRecord({ status: this.progress.step }); - ImporterWebsocket.progressUpdated(this.progress); + this.reportProgress(); return this.progress; } + reloadCount() { + if (!this.importRecord.count) { + this.progress.count.total = 0; + this.progress.count.completed = 0; + } + + this.progress.count.total = this.importRecord.count.total || 0; + this.progress.count.completed = this.importRecord.count.completed || 0; + } + /** * Adds the passed in value to the total amount of items needed to complete. * @@ -295,12 +293,19 @@ export class Base { this.updateRecord({ 'count.completed': this.progress.count.completed }); } - ImporterWebsocket.progressUpdated(this.progress); + this.reportProgress(); this.logger.log(`${ this.progress.count.completed } messages imported`); return this.progress; } + /** + * Sends an updated progress to the websocket + */ + reportProgress() { + ImporterWebsocket.progressUpdated(this.progress); + } + /** * Registers error information on a specific user from the import record * @@ -388,7 +393,7 @@ export class Base { if (err) { throw new Error(err); } else { - const url = file.url.replace(Meteor.absoluteUrl(), '/'); + const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`); const attachment = { title: file.name, diff --git a/app/importer/server/index.js b/app/importer/server/index.js index 489bcabf5c87b..0fed91e5b6608 100644 --- a/app/importer/server/index.js +++ b/app/importer/server/index.js @@ -1,5 +1,4 @@ import { Base } from './classes/ImporterBase'; -import { Imports } from './models/Imports'; import { ImporterWebsocket } from './classes/ImporterWebsocket'; import { Progress } from './classes/ImporterProgress'; import { RawImports } from './models/RawImports'; @@ -10,10 +9,6 @@ import { ProgressStep } from '../lib/ImporterProgressStep'; import { ImporterInfo } from '../lib/ImporterInfo'; import { Importers } from '../lib/Importers'; import './methods/getImportProgress'; -import './methods/getSelectionData'; -import './methods/prepareImport'; -import './methods/restartImport'; -import './methods/setupImporter'; import './methods/startImport'; import './methods/uploadImportFile'; import './methods/getImportFileData'; @@ -24,7 +19,6 @@ import './startup/store'; export { Base, - Imports, Importers, ImporterInfo, ImporterWebsocket, diff --git a/app/importer/server/methods/downloadPublicImportFile.js b/app/importer/server/methods/downloadPublicImportFile.js index 914a324b99506..fa8a20a6f73cc 100644 --- a/app/importer/server/methods/downloadPublicImportFile.js +++ b/app/importer/server/methods/downloadPublicImportFile.js @@ -1,17 +1,18 @@ import http from 'http'; +import https from 'https'; import fs from 'fs'; import { Meteor } from 'meteor/meteor'; - import { RocketChatImportFileInstance } from '../startup/store'; import { ProgressStep } from '../../lib/ImporterProgressStep'; -import { hasRole } from '../../../authorization'; +import { hasPermission } from '../../../authorization'; import { Importers } from '..'; function downloadHttpFile(fileUrl, writeStream) { - http.get(fileUrl, function(response) { + const protocol = fileUrl.startsWith('https') ? https : http; + protocol.get(fileUrl, function(response) { response.pipe(writeStream); }); } @@ -29,8 +30,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'downloadPublicImportFile' }); } - if (!hasRole(userId, 'admin')) { - throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'downloadPublicImportFile' }); + if (!hasPermission(userId, 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'downloadPublicImportFile' }); } const importer = Importers.get(importerKey); @@ -38,39 +39,47 @@ Meteor.methods({ throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'downloadPublicImportFile' }); } + const isUrl = fileUrl.startsWith('http'); + + // Check if it's a valid url or path before creating a new import record + if (!isUrl) { + if (!fs.existsSync(fileUrl)) { + throw new Meteor.Error('error-import-file-missing', fileUrl, { method: 'downloadPublicImportFile' }); + } + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1); const date = new Date(); const dateStr = `${ date.getUTCFullYear() }${ date.getUTCMonth() }${ date.getUTCDate() }${ date.getUTCHours() }${ date.getUTCMinutes() }${ date.getUTCSeconds() }`; const newFileName = `${ dateStr }_${ userId }_${ oldFileName }`; + // Store the file name on the imports collection importer.instance.startFileUpload(newFileName); - importer.instance.updateProgress(ProgressStep.DOWNLOADING_FILE_URL); + importer.instance.updateProgress(ProgressStep.DOWNLOADING_FILE); const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName); - if (fileUrl.startsWith('http')) { + writeStream.on('error', Meteor.bindEnvironment(() => { + importer.instance.updateProgress(ProgressStep.ERROR); + })); + + writeStream.on('end', Meteor.bindEnvironment(() => { + importer.instance.updateProgress(ProgressStep.FILE_LOADED); + })); + + if (isUrl) { downloadHttpFile(fileUrl, writeStream); } else { - if (!fs.existsSync(fileUrl)) { - throw new Meteor.Error('error-import-file-missing', fileUrl, { method: 'downloadPublicImportFile' }); - } - // If the url is actually a folder path on the current machine, skip moving it to the file store if (fs.statSync(fileUrl).isDirectory()) { importer.instance.updateRecord({ file: fileUrl }); - importer.instance.updateProgress(ProgressStep.DOWNLOAD_COMPLETE); + importer.instance.updateProgress(ProgressStep.FILE_LOADED); return; } copyLocalFile(fileUrl, writeStream); } - - writeStream.on('error', Meteor.bindEnvironment(() => { - importer.instance.updateProgress(ProgressStep.ERROR); - })); - - writeStream.on('end', Meteor.bindEnvironment(() => { - importer.instance.updateProgress(ProgressStep.DOWNLOAD_COMPLETE); - })); }, }); diff --git a/app/importer/server/methods/getImportFileData.js b/app/importer/server/methods/getImportFileData.js index 9265313d9cd54..58b5a2439b9fe 100644 --- a/app/importer/server/methods/getImportFileData.js +++ b/app/importer/server/methods/getImportFileData.js @@ -4,34 +4,40 @@ import fs from 'fs'; import { Meteor } from 'meteor/meteor'; import { RocketChatImportFileInstance } from '../startup/store'; -import { hasRole } from '../../../authorization'; +import { hasPermission } from '../../../authorization'; +import { Imports } from '../../../models'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { Importers } from '..'; Meteor.methods({ - getImportFileData(importerKey) { + getImportFileData() { const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getImportFileData' }); } - if (!hasRole(userId, 'admin')) { - throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'getImportFileData' }); + if (!hasPermission(userId, 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'getImportFileData' }); } + const operation = Imports.findLastImport(); + if (!operation) { + throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'getImportFileData' }); + } + + const { importerKey } = operation; + const importer = Importers.get(importerKey); if (!importer) { throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'getImportFileData' }); } - if (!importer.instance) { - return undefined; - } + importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap const waitingSteps = [ - ProgressStep.DOWNLOADING_FILE_URL, + ProgressStep.DOWNLOADING_FILE, ProgressStep.PREPARING_CHANNELS, ProgressStep.PREPARING_MESSAGES, ProgressStep.PREPARING_USERS, diff --git a/app/importer/server/methods/getImportProgress.js b/app/importer/server/methods/getImportProgress.js index a7f5f33d4bed4..220e2e2519f7d 100644 --- a/app/importer/server/methods/getImportProgress.js +++ b/app/importer/server/methods/getImportProgress.js @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; +import { Imports } from '../../../models'; import { Importers } from '..'; Meteor.methods({ - getImportProgress(key) { + getImportProgress() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getImportProgress' }); } @@ -15,15 +16,18 @@ Meteor.methods({ throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' }); } - const importer = Importers.get(key); + const operation = Imports.findLastImport(); + if (!operation) { + throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'getImportProgress' }); + } + const { importerKey } = operation; + const importer = Importers.get(importerKey); if (!importer) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'getImportProgress' }); + throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'getImportProgress' }); } - if (!importer.instance) { - return undefined; - } + importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap return importer.instance.getProgress(); }, diff --git a/app/importer/server/methods/getLatestImportOperations.js b/app/importer/server/methods/getLatestImportOperations.js index af710a67ecc1b..14980ace21b60 100644 --- a/app/importer/server/methods/getLatestImportOperations.js +++ b/app/importer/server/methods/getLatestImportOperations.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Imports } from '../models/Imports'; +import { Imports } from '../../../models'; import { hasRole } from '../../../authorization'; Meteor.methods({ diff --git a/app/importer/server/methods/getSelectionData.js b/app/importer/server/methods/getSelectionData.js deleted file mode 100644 index b49573f1487c4..0000000000000 --- a/app/importer/server/methods/getSelectionData.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; - -import { - Importers, - ProgressStep, -} from '..'; - - -Meteor.methods({ - getSelectionData(key) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSelectionData' }); - } - - if (!hasPermission(Meteor.userId(), 'run-import')) { - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' }); - } - - const importer = Importers.get(key); - - if (!importer || !importer.instance) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'getSelectionData' }); - } - - const progress = importer.instance.getProgress(); - - switch (progress.step) { - case ProgressStep.USER_SELECTION: - return importer.instance.getSelection(); - default: - return undefined; - } - }, -}); diff --git a/app/importer/server/methods/prepareImport.js b/app/importer/server/methods/prepareImport.js deleted file mode 100644 index 9f7b763c61cad..0000000000000 --- a/app/importer/server/methods/prepareImport.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; - -import { Importers } from '..'; - - -Meteor.methods({ - prepareImport(key, dataURI, contentType, fileName) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'prepareImport' }); - } - - if (!hasPermission(Meteor.userId(), 'run-import')) { - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' }); - } - - check(key, String); - check(dataURI, String); - check(fileName, String); - - const importer = Importers.get(key); - - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'prepareImport' }); - } - - const results = importer.instance.prepare(dataURI, contentType, fileName); - - if (results instanceof Promise) { - return results.catch((e) => { throw new Meteor.Error(e); }); - } - return results; - }, -}); diff --git a/app/importer/server/methods/restartImport.js b/app/importer/server/methods/restartImport.js deleted file mode 100644 index f8fb60dcd48e9..0000000000000 --- a/app/importer/server/methods/restartImport.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; - -import { - Importers, - ProgressStep, -} from '..'; - - -Meteor.methods({ - restartImport(key) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'restartImport' }); - } - - if (!hasPermission(Meteor.userId(), 'run-import')) { - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' }); - } - - const importer = Importers.get(key); - - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'restartImport' }); - } - - if (importer.instance) { - if (importer.instance.importRecord) { - importer.instance.updateProgress(ProgressStep.CANCELLED); - importer.instance.updateRecord({ valid: false }); - } - importer.instance = undefined; - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - return importer.instance.getProgress(); - }, -}); diff --git a/app/importer/server/methods/setupImporter.js b/app/importer/server/methods/setupImporter.js deleted file mode 100644 index a5f8c3cfa1f19..0000000000000 --- a/app/importer/server/methods/setupImporter.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; - -import { Importers } from '..'; - - -Meteor.methods({ - setupImporter(key) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setupImporter' }); - } - - if (!hasPermission(Meteor.userId(), 'run-import')) { - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' }); - } - - const importer = Importers.get(key); - - if (!importer) { - console.warn(`Tried to setup ${ name } as an importer.`); - throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'setupImporter' }); - } - - if (importer.instance) { - return importer.instance.getProgress(); - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - return importer.instance.getProgress(); - }, -}); diff --git a/app/importer/server/methods/startImport.js b/app/importer/server/methods/startImport.js index 540b06bec0848..c6651b4d38879 100644 --- a/app/importer/server/methods/startImport.js +++ b/app/importer/server/methods/startImport.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; +import { Imports } from '../../../models'; import { Importers, @@ -11,7 +12,7 @@ import { Meteor.methods({ - startImport(key, input) { + startImport(input) { // Takes name and object with users / channels selected to import if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'startImport' }); @@ -21,16 +22,19 @@ Meteor.methods({ throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'startImport' }); } - if (!key) { - throw new Meteor.Error('error-invalid-importer', `No defined importer by: "${ key }"`, { method: 'startImport' }); + const operation = Imports.findLastImport(); + if (!operation) { + throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'startImport' }); } - const importer = Importers.get(key); - - if (!importer || !importer.instance) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'startImport' }); + const { importerKey } = operation; + const importer = Importers.get(importerKey); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'startImport' }); } + importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap + const usersSelection = input.users.map((user) => new SelectionUser(user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import)); const channelsSelection = input.channels.map((channel) => new SelectionChannel(channel.channel_id, channel.name, channel.is_archived, channel.do_import)); diff --git a/app/importer/server/methods/uploadImportFile.js b/app/importer/server/methods/uploadImportFile.js index d83776530ca49..00baa31c8bbf0 100644 --- a/app/importer/server/methods/uploadImportFile.js +++ b/app/importer/server/methods/uploadImportFile.js @@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file'; import { RocketChatImportFileInstance } from '../startup/store'; -import { hasRole } from '../../../authorization'; +import { hasPermission } from '../../../authorization'; +import { ProgressStep } from '../../lib/ImporterProgressStep'; import { Importers } from '..'; @@ -15,8 +16,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'uploadImportFile' }); } - if (!hasRole(userId, 'admin')) { - throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'uploadImportFile' }); + if (!hasPermission(userId, 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'uploadImportFile' }); } const importer = Importers.get(importerKey); @@ -24,16 +25,24 @@ Meteor.methods({ throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'uploadImportFile' }); } + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + const date = new Date(); const dateStr = `${ date.getUTCFullYear() }${ date.getUTCMonth() }${ date.getUTCDate() }${ date.getUTCHours() }${ date.getUTCMinutes() }${ date.getUTCSeconds() }`; const newFileName = `${ dateStr }_${ userId }_${ fileName }`; + // Store the file name and content type on the imports collection importer.instance.startFileUpload(newFileName, contentType); - const file = new Buffer(binaryContent, 'binary'); + // Save the file on the File Store + const file = new Buffer(binaryContent, 'base64'); const readStream = RocketChatFile.bufferToStream(file); const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); + writeStream.on('end', Meteor.bindEnvironment(() => { + importer.instance.updateProgress(ProgressStep.FILE_LOADED); + })); + readStream.pipe(writeStream); }, }); diff --git a/app/importer/server/models/Imports.js b/app/importer/server/models/Imports.js deleted file mode 100644 index 1bba254a81cad..0000000000000 --- a/app/importer/server/models/Imports.js +++ /dev/null @@ -1,32 +0,0 @@ -import { ProgressStep } from '../../lib/ImporterProgressStep'; -import { Base } from '../../../models'; - -class ImportsModel extends Base { - constructor() { - super('import'); - } - - findPendingImport(key) { - // Finds the latest import operation - const data = this.findOne({ importerKey: key }, { createdAt: -1 }); - if (!data || !data.status) { - return data; - } - - // But only returns it if it is still pending - - const forbiddenStatus = [ - ProgressStep.DONE, - ProgressStep.ERROR, - ProgressStep.CANCELLED, - ]; - - if (forbiddenStatus.indexOf(data.status) >= 0) { - return undefined; - } - - return data; - } -} - -export const Imports = new ImportsModel(); diff --git a/app/importer/server/startup/setImportsToInvalid.js b/app/importer/server/startup/setImportsToInvalid.js index 2622f2eb6c062..c5303516cb597 100644 --- a/app/importer/server/startup/setImportsToInvalid.js +++ b/app/importer/server/startup/setImportsToInvalid.js @@ -1,18 +1,40 @@ import { Meteor } from 'meteor/meteor'; -import { Imports } from '../models/Imports'; +import { Imports } from '../../../models'; import { RawImports } from '../models/RawImports'; +import { ProgressStep } from '../../lib/ImporterProgressStep'; -Meteor.startup(function() { - // Make sure all imports are marked as invalid, data clean up since you can't - // restart an import at the moment. - Imports.update({ valid: { $ne: false } }, { $set: { valid: false } }, { multi: true }); - - // Clean up all the raw import data, since you can't restart an import at the moment +function runDrop(fn) { try { - RawImports.model.rawCollection().drop(); + fn(); } catch (e) { console.log('errror', e); // TODO: Remove // ignored } +} + +Meteor.startup(function() { + const lastOperation = Imports.findLastImport(); + let idToKeep = false; + + // If the operation is ready to start, or already started but failed + // And there's still data for it on the temp collection + // Then we can keep the data there to let the user try again + if (lastOperation && [ProgressStep.USER_SELECTION, ProgressStep.ERROR].includes(lastOperation.status)) { + if (RawImports.find({ import: lastOperation._id }).count() > 0) { + idToKeep = lastOperation._id; + } + } + + if (idToKeep) { + Imports.invalidateOperationsExceptId(idToKeep); + + // Clean up all the raw import data, except for the last operation + runDrop(() => RawImports.model.rawCollection().remove({ import: { $ne: idToKeep } })); + } else { + Imports.invalidateAllOperations(); + + // Clean up all the raw import data + runDrop(() => RawImports.model.rawCollection().drop()); + } }); diff --git a/app/integrations/client/route.js b/app/integrations/client/route.js index 7ee526094c701..18cd9743bfe65 100644 --- a/app/integrations/client/route.js +++ b/app/integrations/client/route.js @@ -3,9 +3,7 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout'; import { t } from '../../utils'; -const dynamic = () => { - import('./views'); -}; +const dynamic = () => import('./views'); FlowRouter.route('/admin/integrations', { name: 'admin-integrations', diff --git a/app/integrations/client/views/integrationsIncoming.html b/app/integrations/client/views/integrationsIncoming.html index 6fef78f2712e0..d9aae546f7004 100644 --- a/app/integrations/client/views/integrationsIncoming.html +++ b/app/integrations/client/views/integrationsIncoming.html @@ -124,7 +124,7 @@
- {{#nrr nrrargs 'message' msg=exampleMsg settings=exampleSettings u=exampleUser}}{{/nrr}} + {{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
diff --git a/app/integrations/client/views/integrationsOutgoing.html b/app/integrations/client/views/integrationsOutgoing.html index 50ef61c66880a..18d6fc8d5791f 100644 --- a/app/integrations/client/views/integrationsOutgoing.html +++ b/app/integrations/client/views/integrationsOutgoing.html @@ -171,7 +171,7 @@
- {{#nrr nrrargs 'message' msg=exampleMsg settings=exampleSettings u=exampleUser}}{{/nrr}} + {{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
{{/if}} diff --git a/app/integrations/server/api/api.js b/app/integrations/server/api/api.js index f645ea574e7b9..db79dc0943bdf 100644 --- a/app/integrations/server/api/api.js +++ b/app/integrations/server/api/api.js @@ -333,7 +333,7 @@ class WebHookAPI extends APIClass { There is only one generic route propagated to Restivus which has URL-path-parameters for the integration and the token. Since the rate-limiter operates on absolute routes, we need to add a limiter to the absolute url before we can validate it */ - enforceRateLimit(objectForRateLimitMatch, request, response) { + enforceRateLimit(objectForRateLimitMatch, request, response, userId) { const { method, url } = request; const route = url.replace(`/${ this.apiPath }`, ''); const nameRoute = this.getFullRouteName(route, [method.toLowerCase()]); @@ -354,7 +354,7 @@ class WebHookAPI extends APIClass { const integrationForRateLimitMatch = objectForRateLimitMatch; integrationForRateLimitMatch.route = nameRoute; - super.enforceRateLimit(integrationForRateLimitMatch, request, response); + super.enforceRateLimit(integrationForRateLimitMatch, request, response, userId); } } diff --git a/app/integrations/server/index.js b/app/integrations/server/index.js index 65e7e01cb60a1..b022e3034fea1 100644 --- a/app/integrations/server/index.js +++ b/app/integrations/server/index.js @@ -1,8 +1,6 @@ import '../lib/rocketchat'; import './logger'; import './lib/validation'; -import './publications/integrations'; -import './publications/integrationHistory'; import './methods/incoming/addIncomingIntegration'; import './methods/incoming/updateIncomingIntegration'; import './methods/incoming/deleteIncomingIntegration'; diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js index 866a5a10166b9..04c4227fa0230 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -491,22 +491,8 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler { } } - executeTriggers(...args) { - logger.outgoing.debug('Execute Trigger:', args[0]); - - const argObject = this.eventNameArgumentsToObject(...args); - const { event, message, room } = argObject; - - // Each type of event should have an event and a room attached, otherwise we - // wouldn't know how to handle the trigger nor would we have anywhere to send the - // result of the integration - if (!event) { - return; - } - + getTriggersToExecute(room, message) { const triggersToExecute = []; - - logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any'); if (room) { switch (room.t) { case 'd': @@ -573,6 +559,25 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler { break; } } + return triggersToExecute; + } + + executeTriggers(...args) { + logger.outgoing.debug('Execute Trigger:', args[0]); + + const argObject = this.eventNameArgumentsToObject(...args); + const { event, message, room } = argObject; + + // Each type of event should have an event and a room attached, otherwise we + // wouldn't know how to handle the trigger nor would we have anywhere to send the + // result of the integration + if (!event) { + return; + } + + logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any'); + + const triggersToExecute = this.getTriggersToExecute(room, message); if (this.triggers.__any) { // For outgoing integration which don't rely on rooms. diff --git a/app/integrations/server/publications/integrationHistory.js b/app/integrations/server/publications/integrationHistory.js deleted file mode 100644 index cc2ea926a2354..0000000000000 --- a/app/integrations/server/publications/integrationHistory.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasAtLeastOnePermission } from '../../../authorization/server'; -import { IntegrationHistory } from '../../../models/server'; -import { mountIntegrationHistoryQueryBasedOnPermissions } from '../lib/mountQueriesBasedOnPermission'; - -Meteor.publish('integrationHistory', function _integrationHistoryPublication(integrationId, limit = 25) { - console.warn('The publication "integrationHistory" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - if (!hasAtLeastOnePermission(this.userId, [ - 'manage-outgoing-integrations', - 'manage-own-outgoing-integrations', - ])) { - throw new Meteor.Error('not-authorized'); - } - - return IntegrationHistory.find(Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(this.userId, integrationId)), { sort: { _updatedAt: -1 }, limit }); -}); diff --git a/app/integrations/server/publications/integrations.js b/app/integrations/server/publications/integrations.js deleted file mode 100644 index 48ebe040b380f..0000000000000 --- a/app/integrations/server/publications/integrations.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasAtLeastOnePermission } from '../../../authorization/server'; -import { Integrations } from '../../../models/server'; -import { mountIntegrationQueryBasedOnPermissions } from '../lib/mountQueriesBasedOnPermission'; - -Meteor.publish('integrations', function _integrationPublication() { - console.warn('The publication "integrations" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - - if (!hasAtLeastOnePermission(this.userId, [ - 'manage-outgoing-integrations', - 'manage-own-outgoing-integrations', - 'manage-incoming-integrations', - 'manage-own-incoming-integrations', - ])) { - throw new Meteor.Error('not-authorized'); - } - - return Integrations.find(mountIntegrationQueryBasedOnPermissions(this.userId)); -}); diff --git a/app/katex/katex.min.css b/app/katex/katex.min.css index 631f6c1d67468..c0cd1451ae6df 100644 --- a/app/katex/katex.min.css +++ b/app/katex/katex.min.css @@ -1 +1 @@ -@font-face{font-family:KaTeX_AMS;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Script;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size1;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size2;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size3;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size4;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Typewriter;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");font-weight:400;font-style:normal}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:inline-block;text-align:initial}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;white-space:nowrap;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-html{display:inline-block}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .base{position:relative}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathit{font-family:KaTeX_Math;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr{font-family:KaTeX_Script}.katex .mathsf{font-family:KaTeX_SansSerif}.katex .mainit{font-family:KaTeX_Main;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%}.katex .mspace{display:inline-block}.katex .mspace.negativethinspace{margin-left:-.16667em}.katex .mspace.muspace{width:.055556em}.katex .mspace.thinspace{width:.16667em}.katex .mspace.negativemediumspace{margin-left:-.22222em}.katex .mspace.mediumspace{width:.22222em}.katex .mspace.thickspace{width:.27778em}.katex .mspace.sixmuspace{width:.333333em}.katex .mspace.eightmuspace{width:.444444em}.katex .mspace.enspace{width:.5em}.katex .mspace.twelvemuspace{width:.666667em}.katex .mspace.quad{width:1em}.katex .mspace.qquad{width:2em}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer,.katex .sizing{display:inline-block}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{width:0;position:relative}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;margin:0 -.125em;width:.25em;overflow:hidden;position:relative}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex .vertical-separator svg{width:.25em}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox{box-sizing:border-box;border:.04em solid #000}.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap+.mbin,.katex .cancel-lap+.mord,.katex .cancel-lap+.msupsub,.katex .mbin+.cancel-lap,.katex .mord+.cancel-lap{margin-left:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em} \ No newline at end of file +@font-face{font-family:KaTeX_AMS;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Script;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size1;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size2;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size3;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size4;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Typewriter;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");font-weight:400;font-style:normal}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-version:after{content:"0.11.1"}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathdefault{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px;min-width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline{display:inline-block;width:100%;border-bottom-style:dashed}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .op-limits>.vlist-t{text-align:center}.katex .accent>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;height:inherit;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex img{border-style:none;min-width:0;min-height:0;max-width:none;max-height:none}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{text-align:left} diff --git a/app/lazy-load/client/index.js b/app/lazy-load/client/index.js index 248ff4f3bc698..61a0ece9c7354 100644 --- a/app/lazy-load/client/index.js +++ b/app/lazy-load/client/index.js @@ -1,43 +1,46 @@ -import { Blaze } from 'meteor/blaze'; -import _ from 'underscore'; import './lazyloadImage'; -const getEl = (el, instance) => (instance && instance.firstNode) || el; +const map = new WeakMap(); -const loadImage = (el, instance) => { - const element = getEl(el, instance); +const featureExists = !!window.IntersectionObserver; +const loadImage = (el) => { + const instance = map.get(el); + + map.delete(el); + + if (!instance) { + return instance.loaded.set(true); + } const img = new Image(); - const src = element.getAttribute('data-src'); + const src = el.getAttribute('data-src'); img.onload = () => { - if (instance) { - instance.loaded.set(true); - } else { - element.className = element.className.replace('lazy-img', ''); - element.src = src; - } - element.removeAttribute('data-src'); + el.className = el.className.replace('lazy-img', ''); + el.src = src; + el.removeAttribute('data-src'); }; img.src = src; }; -const isVisible = (el, instance) => { - requestAnimationFrame(() => { - const rect = getEl(el, instance).getBoundingClientRect(); - if (rect.top >= -100 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)) { - return loadImage(el, instance); +const observer = featureExists && new IntersectionObserver( + (entries, observer) => entries.forEach((entry) => { + if (entry.isIntersecting) { + observer.unobserve(entry.target); + return loadImage(entry.target); } - }); -}; + }) + , + { + threshold: [0], + trackVisibility: true, + delay: 230, + }, +); -window.addEventListener('resize', window.lazyloadtick); - -export const lazyloadtick = _.debounce(() => { - const lazyImg = document.querySelectorAll('.lazy-img[data-src]'); - Array.from(lazyImg).forEach((el) => - isVisible(el, Blaze.getView(el)._templateInstance), - ); -}, 300); - -window.lazyloadtick = lazyloadtick; - -export const addImage = (instance) => isVisible(instance.firstNode, instance); +export const addImage = (instance) => { + const el = instance.firstNode; + map.set(el, instance); + if (featureExists) { + return observer.observe(el); + } + loadImage(el); +}; diff --git a/app/lib/lib/MessageTypes.js b/app/lib/lib/MessageTypes.js index 012d76f0360cf..3f6df60904161 100644 --- a/app/lib/lib/MessageTypes.js +++ b/app/lib/lib/MessageTypes.js @@ -160,3 +160,46 @@ Meteor.startup(function() { }, }); }); + +export const MessageTypesValues = [ + { + key: 'uj', + i18nLabel: 'Message_HideType_uj', + }, { + key: 'ul', + i18nLabel: 'Message_HideType_ul', + }, { + key: 'ru', + i18nLabel: 'Message_HideType_ru', + }, { + key: 'au', + i18nLabel: 'Message_HideType_au', + }, { + key: 'mute_unmute', + i18nLabel: 'Message_HideType_mute_unmute', + }, { + key: 'r', + i18nLabel: 'Message_HideType_r', + }, { + key: 'ut', + i18nLabel: 'Message_HideType_ut', + }, { + key: 'wm', + i18nLabel: 'Message_HideType_wm', + }, { + key: 'rm', + i18nLabel: 'Message_HideType_rm', + }, { + key: 'subscription_role_added', + i18nLabel: 'Message_HideType_subscription_role_added', + }, { + key: 'subscription_role_removed', + i18nLabel: 'Message_HideType_subscription_role_removed', + }, { + key: 'room_archived', + i18nLabel: 'Message_HideType_room_archived', + }, { + key: 'room_unarchived', + i18nLabel: 'Message_HideType_room_unarchived', + }, +]; diff --git a/app/lib/lib/roomTypes/private.js b/app/lib/lib/roomTypes/private.js index ee095a937f2a3..a418a4d436fc8 100644 --- a/app/lib/lib/roomTypes/private.js +++ b/app/lib/lib/roomTypes/private.js @@ -90,9 +90,9 @@ export class PrivateRoomType extends RoomTypeConfig { return !room.broadcast; case RoomSettingsEnum.REACT_WHEN_READ_ONLY: return !room.broadcast && room.ro; - case RoomSettingsEnum.SYSTEM_MESSAGES: case RoomSettingsEnum.E2E: return settings.get('E2E_Enable') === true; + case RoomSettingsEnum.SYSTEM_MESSAGES: default: return true; } diff --git a/app/lib/server/functions/createRoom.js b/app/lib/server/functions/createRoom.js index 116c6f5e92dcc..434ecc8ac0862 100644 --- a/app/lib/server/functions/createRoom.js +++ b/app/lib/server/functions/createRoom.js @@ -57,7 +57,6 @@ export const createRoom = function(type, name, owner, members, readOnly, extraDa }, extraData, { ts: now, ro: readOnly === true, - sysMes: readOnly !== true, }); if (type === 'd') { diff --git a/app/lib/server/functions/deleteMessage.js b/app/lib/server/functions/deleteMessage.js index e74ff6d7d1bbf..4e80b52fb1147 100644 --- a/app/lib/server/functions/deleteMessage.js +++ b/app/lib/server/functions/deleteMessage.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { FileUpload } from '../../../file-upload'; -import { settings } from '../../../settings'; -import { Messages, Uploads, Rooms } from '../../../models'; -import { Notifications } from '../../../notifications'; -import { callbacks } from '../../../callbacks'; +import { FileUpload } from '../../../file-upload/server'; +import { settings } from '../../../settings/server'; +import { Messages, Uploads, Rooms } from '../../../models/server'; +import { Notifications } from '../../../notifications/server'; +import { callbacks } from '../../../callbacks/server'; import { Apps } from '../../../apps/server'; export const deleteMessage = function(message, user) { diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index 031ce3c612eb4..4962c5522f064 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -12,6 +12,10 @@ export const deleteUser = function(userId) { fields: { username: 1, avatarOrigin: 1, federation: 1 }, }); + if (!user) { + return; + } + if (user.federation) { const existingSubscriptions = Subscriptions.find({ 'u._id': user._id }).count(); diff --git a/app/lib/server/functions/getAvatarSuggestionForUser.js b/app/lib/server/functions/getAvatarSuggestionForUser.js index 2960145bfb9db..f2bb8cd473065 100644 --- a/app/lib/server/functions/getAvatarSuggestionForUser.js +++ b/app/lib/server/functions/getAvatarSuggestionForUser.js @@ -5,97 +5,132 @@ import { ServiceConfiguration } from 'meteor/service-configuration'; import { settings } from '../../../settings'; -export function getAvatarSuggestionForUser(user) { - check(user, Object); - - const avatars = []; - - if (user.services && user.services.facebook && user.services.facebook.id && settings.get('Accounts_OAuth_Facebook')) { - avatars.push({ - service: 'facebook', - url: `https://graph.facebook.com/${ user.services.facebook.id }/picture?type=large`, - }); - } - - if (user.services && user.services.google && user.services.google.picture && user.services.google.picture !== 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg' && settings.get('Accounts_OAuth_Google')) { - avatars.push({ - service: 'google', - url: user.services.google.picture, - }); - } - - if (user.services && user.services.github && user.services.github.username && settings.get('Accounts_OAuth_Github')) { - avatars.push({ - service: 'github', - url: `https://avatars.githubusercontent.com/${ user.services.github.username }?s=200`, - }); - } - - if (user.services && user.services.linkedin && user.services.linkedin.pictureUrl && settings.get('Accounts_OAuth_Linkedin')) { - avatars.push({ - service: 'linkedin', - url: user.services.linkedin.pictureUrl, - }); - } - - if (user.services && user.services.twitter && user.services.twitter.profile_image_url_https && settings.get('Accounts_OAuth_Twitter')) { - avatars.push({ - service: 'twitter', - url: user.services.twitter.profile_image_url_https.replace(/_normal|_bigger/, ''), - }); - } - - if (user.services && user.services.gitlab && user.services.gitlab.avatar_url && settings.get('Accounts_OAuth_Gitlab')) { - avatars.push({ - service: 'gitlab', - url: user.services.gitlab.avatar_url, - }); - } - - if (user.services && user.services.blockstack && user.services.blockstack.image && settings.get('Blockstack_Enable')) { - avatars.push({ - service: 'blockstack', - url: user.services.blockstack.image, - }); - } - - for (const service in user.services) { - if (user.services[service]._OAuthCustom) { - const services = ServiceConfiguration.configurations.find({ service }, { fields: { secret: 0 } }).fetch(); +const avatarProviders = { + facebook(user) { + if (user.services && user.services.facebook && user.services.facebook.id && settings.get('Accounts_OAuth_Facebook')) { + return { + service: 'facebook', + url: `https://graph.facebook.com/${ user.services.facebook.id }/picture?type=large`, + }; + } + }, + + google(user) { + if (user.services && user.services.google && user.services.google.picture && user.services.google.picture !== 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg' && settings.get('Accounts_OAuth_Google')) { + return { + service: 'google', + url: user.services.google.picture, + }; + } + }, + + github(user) { + if (user.services && user.services.github && user.services.github.username && settings.get('Accounts_OAuth_Github')) { + return { + service: 'github', + url: `https://avatars.githubusercontent.com/${ user.services.github.username }?s=200`, + }; + } + }, + + linkedin(user) { + if (user.services && user.services.linkedin && user.services.linkedin.pictureUrl && settings.get('Accounts_OAuth_Linkedin')) { + return { + service: 'linkedin', + url: user.services.linkedin.pictureUrl, + }; + } + }, + + twitter(user) { + if (user.services && user.services.twitter && user.services.twitter.profile_image_url_https && settings.get('Accounts_OAuth_Twitter')) { + return { + service: 'twitter', + url: user.services.twitter.profile_image_url_https.replace(/_normal|_bigger/, ''), + }; + } + }, + + gitlab(user) { + if (user.services && user.services.gitlab && user.services.gitlab.avatar_url && settings.get('Accounts_OAuth_Gitlab')) { + return { + service: 'gitlab', + url: user.services.gitlab.avatar_url, + }; + } + }, + + blockstack(user) { + if (user.services && user.services.blockstack && user.services.blockstack.image && settings.get('Blockstack_Enable')) { + return { + service: 'blockstack', + url: user.services.blockstack.image, + }; + } + }, + + customOAuth(user) { + const avatars = []; + for (const service in user.services) { + if (user.services[service]._OAuthCustom) { + const services = ServiceConfiguration.configurations.find({ service }, { fields: { secret: 0 } }).fetch(); + + if (services.length > 0) { + if (user.services[service].avatarUrl) { + avatars.push({ + service, + url: user.services[service].avatarUrl, + }); + } + } + } + } + return avatars; + }, + + emails(user) { + const avatars = []; + if (user.emails && user.emails.length > 0) { + for (const email of user.emails) { + if (email.verified === true) { + avatars.push({ + service: 'gravatar', + url: Gravatar.imageUrl(email.address, { + default: '404', + size: 200, + secure: true, + }), + }); + } - if (services.length > 0) { - if (user.services[service].avatarUrl) { + if (email.verified !== true) { avatars.push({ - service, - url: user.services[service].avatarUrl, + service: 'gravatar', + url: Gravatar.imageUrl(email.address, { + default: '404', + size: 200, + secure: true, + }), }); } } } - } + return avatars; + }, +}; - if (user.emails && user.emails.length > 0) { - for (const email of user.emails) { - if (email.verified === true) { - avatars.push({ - service: 'gravatar', - url: Gravatar.imageUrl(email.address, { - default: '404', - size: 200, - secure: true, - }), - }); - } +export function getAvatarSuggestionForUser(user) { + check(user, Object); + + const avatars = []; - if (email.verified !== true) { - avatars.push({ - service: 'gravatar', - url: Gravatar.imageUrl(email.address, { - default: '404', - size: 200, - secure: true, - }), - }); + for (const avatarProvider of Object.values(avatarProviders)) { + const avatar = avatarProvider(user); + if (avatar) { + if (Array.isArray(avatar)) { + avatars.push(...avatar); + } else { + avatars.push(avatar); } } } diff --git a/app/lib/server/functions/insertMessage.js b/app/lib/server/functions/insertMessage.js index 35c7ccec1501d..d45b8dcc275d7 100644 --- a/app/lib/server/functions/insertMessage.js +++ b/app/lib/server/functions/insertMessage.js @@ -47,7 +47,7 @@ const validateAttachment = (attachment) => { check(attachment, objectMaybeIncluding({ color: String, text: String, - ts: Match.OneOf(String, Match.Integer), + ts: Match.OneOf(String, Number), thumb_url: String, button_alignment: String, actions: [Match.Any], diff --git a/app/lib/server/functions/loadMessageHistory.js b/app/lib/server/functions/loadMessageHistory.js index 6625128440005..8e381a3fadc16 100644 --- a/app/lib/server/functions/loadMessageHistory.js +++ b/app/lib/server/functions/loadMessageHistory.js @@ -1,27 +1,19 @@ import { settings } from '../../../settings'; -import { Messages } from '../../../models'; +import { Messages, Rooms } from '../../../models'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; -const hideMessagesOfType = []; +const hideMessagesOfTypeServer = new Set(); -settings.get(/Message_HideType_.+/, function(key, value) { - const type = key.replace('Message_HideType_', ''); - const types = type === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [type]; - - return types.forEach((type) => { - const index = hideMessagesOfType.indexOf(type); - - if (value === true && index === -1) { - return hideMessagesOfType.push(type); - } - - if (index > -1) { - return hideMessagesOfType.splice(index, 1); - } - }); +settings.get('Hide_System_Messages', function(key, values) { + const hiddenTypes = values.reduce((array, value) => [...array, ...value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]], []); + hideMessagesOfTypeServer.clear(); + hiddenTypes.forEach((item) => hideMessagesOfTypeServer.add(item)); }); export const loadMessageHistory = function loadMessageHistory({ userId, rid, end, limit = 20, ls }) { + const room = Rooms.findOne(rid, { fields: { sysMes: 1 } }); + + const hiddenMessageTypes = Array.isArray(room && room.sysMes) ? room.sysMes : Array.from(hideMessagesOfTypeServer.values()); // TODO probably remove on chained event system const options = { sort: { ts: -1, @@ -35,12 +27,7 @@ export const loadMessageHistory = function loadMessageHistory({ userId, rid, end }; } - let records; - if (end != null) { - records = Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(rid, end, hideMessagesOfType, options).fetch(); - } else { - records = Messages.findVisibleByRoomIdNotContainingTypes(rid, hideMessagesOfType, options).fetch(); - } + const records = end != null ? Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(rid, end, hiddenMessageTypes, options).fetch() : Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options).fetch(); const messages = normalizeMessagesForUser(records, userId); let unreadNotLoaded = 0; let firstUnread; @@ -50,8 +37,7 @@ export const loadMessageHistory = function loadMessageHistory({ userId, rid, end if ((firstMessage != null ? firstMessage.ts : undefined) > ls) { delete options.limit; - - const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(rid, ls, firstMessage.ts, hideMessagesOfType, { + const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(rid, ls, firstMessage.ts, hiddenMessageTypes, { limit: 1, sort: { ts: 1, diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js index 7e0caef79a9d5..ea5ce3088d51c 100644 --- a/app/lib/server/functions/sendMessage.js +++ b/app/lib/server/functions/sendMessage.js @@ -90,7 +90,7 @@ const validateAttachment = (attachment) => { check(attachment, objectMaybeIncluding({ color: String, text: String, - ts: Match.OneOf(String, Match.Integer), + ts: Match.OneOf(String, Number), thumb_url: ValidFullURLParam, button_alignment: String, actions: [Match.Any], diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index 4e7ec6201c9bc..9cab632e239ac 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -17,6 +17,10 @@ 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: ${ dataURI }`); + throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ 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 (${ dataURI }) for ${ user.username }:`, error); @@ -26,12 +30,12 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { if (result.statusCode !== 200) { console.log(`Not a valid response, ${ result.statusCode }, from the avatar url: ${ dataURI }`); - throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'RocketChat.setUserAvatar', 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 }`); - throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'RocketChat.setUserAvatar', url: dataURI }); + throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI }); } encoding = 'binary'; diff --git a/app/lib/server/methods/executeSlashCommandPreview.js b/app/lib/server/methods/executeSlashCommandPreview.js index 887721dcfb113..52f2c18527a5c 100644 --- a/app/lib/server/methods/executeSlashCommandPreview.js +++ b/app/lib/server/methods/executeSlashCommandPreview.js @@ -29,6 +29,6 @@ Meteor.methods({ }); } - return slashCommands.executePreview(command.cmd, command.params, command.msg, preview); + return slashCommands.executePreview(command.cmd, command.params, command.msg, preview, command.triggerId); }, }); diff --git a/app/lib/server/methods/saveSettings.js b/app/lib/server/methods/saveSettings.js index d7cf7bd02ad6e..97c12bd85cac5 100644 --- a/app/lib/server/methods/saveSettings.js +++ b/app/lib/server/methods/saveSettings.js @@ -37,6 +37,9 @@ Meteor.methods({ case 'int': check(value, Number); break; + case 'multiSelect': + check(value, Array); + break; default: check(value, String); break; diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js index 827ad31ba39c8..d25284a3639fa 100644 --- a/app/lib/server/methods/sendMessage.js +++ b/app/lib/server/methods/sendMessage.js @@ -50,6 +50,7 @@ export function executeSendMessage(uid, message) { const user = Users.findOneById(uid, { fields: { username: 1, + type: 1, ...!!settings.get('Message_SetNameToAliasEnabled') && { name: 1 }, }, }); @@ -67,7 +68,7 @@ export function executeSendMessage(uid, message) { } try { - const room = canSendMessage(rid, { uid, username: user.username }); + const room = canSendMessage(rid, { uid, username: user.username, type: user.type }); if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) { message.alias = user.name; } diff --git a/app/lib/server/startup/email.js b/app/lib/server/startup/email.js index d31d4de65a59a..852618fd1a1cb 100644 --- a/app/lib/server/startup/email.js +++ b/app/lib/server/startup/email.js @@ -2,6 +2,10 @@ import { settings } from '../../../settings'; settings.addGroup('Email', function() { this.section('Style', function() { + this.add('email_plain_text_only', false, { + type: 'boolean', + }); + this.add('email_style', `html, body, .body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue','Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Meiryo UI',Arial,sans-serif; } body, .body { @@ -103,6 +107,10 @@ settings.addGroup('Email', function() { multiline: true, i18nLabel: 'email_style_label', i18nDescription: 'email_style_description', + enableQuery: { + _id: 'email_plain_text_only', + value: false, + }, }); }); diff --git a/app/lib/server/startup/rateLimiter.js b/app/lib/server/startup/rateLimiter.js index eeda6db044112..f12f23caa8c85 100644 --- a/app/lib/server/startup/rateLimiter.js +++ b/app/lib/server/startup/rateLimiter.js @@ -5,6 +5,9 @@ import { RateLimiter } from 'meteor/rate-limit'; import { settings } from '../../../settings'; import { metrics } from '../../../metrics'; +import { Logger } from '../../../logger'; + +const logger = new Logger('RateLimiter', {}); // Get initial set of names already registered for rules const names = new Set(Object.values(DDPRateLimiter.printRules()) @@ -107,8 +110,8 @@ const ruleIds = {}; const callback = (message, name) => (reply, input) => { if (reply.allowed === false) { - console.warn('DDP RATE LIMIT:', message); - console.warn(JSON.stringify({ ...reply, ...input }, null, 2)); + logger.info('DDP RATE LIMIT:', message); + logger.info(JSON.stringify({ ...reply, ...input }, null, 2)); 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 7808e707e5885..8bd5252c29787 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -2,6 +2,7 @@ import { Random } from 'meteor/random'; import { settings } from '../../../settings'; import './email'; +import { MessageTypesValues } from '../../lib/MessageTypes'; // Insert server unique id if it doesn't exist settings.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), { @@ -440,11 +441,6 @@ settings.addGroup('Accounts', function() { public: true, i18nLabel: 'Email_Notification_Mode', }); - this.add('Accounts_Default_User_Preferences_roomCounterSidebar', false, { - type: 'boolean', - public: true, - i18nLabel: 'Show_room_counter_on_sidebar', - }); this.add('Accounts_Default_User_Preferences_newRoomNotification', 'door', { type: 'select', values: [ @@ -1098,28 +1094,15 @@ settings.addGroup('Message', function() { type: 'int', public: true, }); - this.add('Message_HideType_uj', false, { - type: 'boolean', - public: true, - }); - this.add('Message_HideType_ul', false, { - type: 'boolean', - public: true, - }); - this.add('Message_HideType_ru', false, { - type: 'boolean', - public: true, - }); - this.add('Message_HideType_au', false, { - type: 'boolean', - public: true, - }); - this.add('Message_HideType_mute_unmute', false, { - type: 'boolean', + + this.add('Hide_System_Messages', [], { + type: 'multiSelect', public: true, + values: MessageTypesValues, }); + this.add('Message_ErasureType', 'Delete', { type: 'select', public: true, @@ -1390,6 +1373,10 @@ settings.addGroup('Layout', function() { type: 'boolean', public: true, }); + return this.add('UI_Show_top_navbar_embedded_layout', false, { + type: 'boolean', + public: true, + }); }); }); diff --git a/app/livechat/client/views/app/livechatCustomFieldForm.html b/app/livechat/client/views/app/livechatCustomFieldForm.html index e90f9c4ab9738..d879ce74218c2 100644 --- a/app/livechat/client/views/app/livechatCustomFieldForm.html +++ b/app/livechat/client/views/app/livechatCustomFieldForm.html @@ -34,6 +34,12 @@ +
+ +
+ +
+
diff --git a/app/livechat/client/views/app/livechatCustomFieldForm.js b/app/livechat/client/views/app/livechatCustomFieldForm.js index 0a821fa4421f2..b768a5babeae2 100644 --- a/app/livechat/client/views/app/livechatCustomFieldForm.js +++ b/app/livechat/client/views/app/livechatCustomFieldForm.js @@ -24,6 +24,7 @@ Template.livechatCustomFieldForm.events({ const label = instance.$('input[name=label]').val(); const scope = instance.$('select[name=scope]').val(); const visibility = instance.$('select[name=visibility]').val(); + const regexp = instance.$('input[name=regexp]').val(); if (!/^[0-9a-zA-Z-_]+$/.test(field)) { return toastr.error(t('error-invalid-custom-field-name')); @@ -41,6 +42,7 @@ Template.livechatCustomFieldForm.events({ label, scope: scope.trim(), visibility: visibility.trim(), + regexp: regexp.trim(), }; Meteor.call('livechat:saveCustomField', _id, customFieldData, function(error) { diff --git a/app/livechat/client/views/app/tabbar/agentInfo.html b/app/livechat/client/views/app/tabbar/agentInfo.html index c3c1cac243845..5c33cc4341afe 100644 --- a/app/livechat/client/views/app/tabbar/agentInfo.html +++ b/app/livechat/client/views/app/tabbar/agentInfo.html @@ -14,10 +14,10 @@

{{name}}

{{#if username}}

@{{username}}

{{/if}} - + {{# userPresence uid=uid}} + {{/userPresence}}
diff --git a/app/livechat/client/views/app/tabbar/agentInfo.js b/app/livechat/client/views/app/tabbar/agentInfo.js index 8d5d04a63e7fe..6894586f32dac 100644 --- a/app/livechat/client/views/app/tabbar/agentInfo.js +++ b/app/livechat/client/views/app/tabbar/agentInfo.js @@ -20,7 +20,9 @@ Template.agentInfo.helpers({ const hasCustomFields = customFieldsTemplate() !== null; return (availableDepartments.length > 0 && hasPermission('add-livechat-department-agents')) || hasCustomFields; }, - + uid() { + return Template.instance().agent.get()._id; + }, name() { const agent = Template.instance().agent.get(); return agent && agent.name ? agent.name : TAPi18n.__('Unnamed'); diff --git a/app/livechat/client/views/app/tabbar/visitorEdit.html b/app/livechat/client/views/app/tabbar/visitorEdit.html index 9f8510b53bfc4..0d3afb3118bec 100644 --- a/app/livechat/client/views/app/tabbar/visitorEdit.html +++ b/app/livechat/client/views/app/tabbar/visitorEdit.html @@ -30,6 +30,17 @@

{{username}}

+ + {{#each visitorCustomFields}} +
+ +
+ {{/each}} {{/with}} {{#with room}} @@ -77,7 +88,19 @@

{{_ "Conversation" }}

{{/each}} + + {{#each roomCustomFields}} +
+ +
+ {{/each}} {{/with}} +
diff --git a/app/livechat/client/views/app/tabbar/visitorEdit.js b/app/livechat/client/views/app/tabbar/visitorEdit.js index 79c9dc1886d73..6e6d64ccb6ec6 100644 --- a/app/livechat/client/views/app/tabbar/visitorEdit.js +++ b/app/livechat/client/views/app/tabbar/visitorEdit.js @@ -8,15 +8,57 @@ import { hasRole } from '../../../../../authorization'; import './visitorEdit.html'; import { APIClient } from '../../../../../utils/client'; +const CUSTOM_FIELDS_COUNT = 100; + Template.visitorEdit.helpers({ visitor() { return Template.instance().visitor.get(); }, + visitorCustomFields() { + const customFields = Template.instance().customFields.get(); + if (!customFields || customFields.length === 0) { + return []; + } + + const fields = []; + const visitor = Template.instance().visitor.get(); + const { livechatData = {} } = visitor || {}; + + customFields.forEach((field) => { + if (field.visibility !== 'hidden' && field.scope === 'visitor') { + const value = livechatData[field._id] ? livechatData[field._id] : ''; + fields.push({ name: field._id, label: field.label, value }); + } + }); + + return fields; + }, + room() { return Template.instance().room.get(); }, + roomCustomFields() { + const customFields = Template.instance().customFields.get(); + if (!customFields || customFields.length === 0) { + return []; + } + + const fields = []; + const room = Template.instance().room.get(); + const { livechatData = {} } = room || {}; + + customFields.forEach((field) => { + if (field.visibility !== 'hidden' && field.scope === 'room') { + const value = livechatData[field._id] ? livechatData[field._id] : ''; + fields.push({ name: field._id, label: field.label, value }); + } + }); + + return fields; + }, + email() { const visitor = Template.instance().visitor.get(); if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { @@ -61,6 +103,7 @@ Template.visitorEdit.onCreated(async function() { this.availableTags = new ReactiveVar([]); this.agentDepartments = new ReactiveVar([]); this.availableUserTags = new ReactiveVar([]); + this.customFields = new ReactiveVar([]); this.autorun(async () => { const { visitorId } = Template.currentData(); @@ -74,8 +117,10 @@ Template.visitorEdit.onCreated(async function() { this.autorun(async () => { const { room } = await APIClient.v1.get(`rooms.info?roomId=${ rid }`); + const { customFields } = await APIClient.v1.get(`livechat/custom-fields?count=${ CUSTOM_FIELDS_COUNT }`); this.room.set(room); this.tags.set((room && room.tags) || []); + this.customFields.set(customFields || []); }); const uid = Meteor.userId(); @@ -106,9 +151,17 @@ Template.visitorEdit.events({ userData.name = event.currentTarget.elements.name.value; userData.email = event.currentTarget.elements.email.value; userData.phone = event.currentTarget.elements.phone.value; + userData.livechatData = {}; + $('[data-visitorLivechatData=true]').each(function() { + userData.livechatData[this.name] = $(this).val() || ''; + }); roomData.topic = event.currentTarget.elements.topic.value; roomData.tags = instance.tags.get(); + roomData.livechatData = {}; + $('[data-roomLivechatData=true]').each(function() { + roomData.livechatData[this.name] = $(this).val() || ''; + }); if (sms) { delete userData.phone; @@ -119,6 +172,7 @@ Template.visitorEdit.events({ toastr.error(t(err.error)); } else { toastr.success(t('Saved')); + this.save(); } }); }, @@ -174,10 +228,6 @@ Template.visitorEdit.events({ } }, - 'click .save'() { - this.save(); - }, - 'click .cancel'() { this.cancel(); }, diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html index 4e0f029df5d53..ca1e6e7fbc086 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.html +++ b/app/livechat/client/views/app/tabbar/visitorInfo.html @@ -10,42 +10,46 @@ {{#with user}}
{{#if name}} -

{{name}}

+

{{name}}

{{username}}

{{else}} -

{{username}}

+

{{username}}

{{/if}}
    {{#if utc}}
  • {{userTime}} (UTC {{utc}})
  • {{/if}} - {{#each visitorEmails}}
  • {{address}}{{#if verified}} {{/if}}
  • {{/each}} - {{#each phone}}
  • {{phoneNumber}}
  • {{/each}} - {{#if lastLogin}}
  • {{_ "Created_at"}}: {{createdAt}}
  • {{/if}} - {{#if lastLogin}}
  • {{_ "Last_login"}}: {{lastLogin}}
  • {{/if}} + {{#each visitorEmails}}
  • {{address}}{{#if verified}} {{/if}}
  • {{/each}} + {{#each phone}}
  • {{phoneNumber}}
  • {{/each}} + {{#if lastLogin}}
  • {{_ "Created_at"}}: {{createdAt}}
  • {{/if}} + {{#if lastLogin}}
  • {{_ "Last_login"}}: {{lastLogin}}
  • {{/if}} {{#if ip}}
  • {{ip}}
  • {{/if}} {{#if os}}
  • {{os}}
  • {{/if}} {{#if browser}}
  • {{browser}}
  • {{/if}} + {{#if customVisitorFields}} + {{#each customVisitorFields}}
  • {{label}}: {{value}}
  • {{/each}} + {{/if}}
{{/with}} - {{#with room}} -
-
    - {{#if facebook}}
  • {{_ "Facebook_Page"}}: {{facebook.page.name}}
  • {{/if}} - {{#if sms}}
  • {{_ "SMS_Enabled"}}
  • {{/if}} - {{#if topic}}
  • {{_ "Topic"}}: {{{RocketChatMarkdown topic}}}
  • {{/if}} - {{#if tags}}
  • {{_ "Tags"}}: {{joinTags}}
  • {{/if}} -
-
- {{/with}} - - {{#with department}} -
-
    - {{#if name}}
  • {{_ "Department"}}: {{name}}
  • {{/if}} -
-
- {{/with}} +
+

{{_ "Conversation"}}

+
    + {{#with room}} + {{#if servedBy}}
  • {{_ "Agent"}}: {{servedBy.username}}
  • {{/if}} + {{#if facebook}}
  • {{_ "Facebook_Page"}}: {{facebook.page.name}}
  • {{/if}} + {{#if sms}}
  • {{_ "SMS_Enabled"}}
  • {{/if}} + {{#if topic}}
  • {{_ "Topic"}}: {{{RocketChatMarkdown topic}}}
  • {{/if}} + {{#if tags}}
  • {{_ "Tags"}}: {{joinTags}}
  • {{/if}} + {{#if closedAt}}
  • {{_ "Closed_At"}}: {{roomClosedDateTime}} {{_ "by"}}: {{roomClosedBy}}
  • {{/if}} + {{/with}} + {{#with department}} + {{#if name}}
  • {{_ "Department"}}: {{name}}
  • {{/if}} + {{/with}} + {{#if customRoomFields}} + {{#each customRoomFields}}
  • {{label}}: {{value}}
  • {{/each}} + {{/if}} +
+
{{#if canSeeButtons}} @@ -68,20 +72,6 @@

{{username}}

{{/if}} - {{#if customFields}} -
-

{{_ "Custom_Fields"}}

- -
-
    - {{#each customFields}} -
  • {{label}}: {{value}}
  • - {{/each}} -
-
-
- {{/if}} - {{> visitorNavigation .}} diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index d52373a34ec60..39275ffebab3d 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -17,6 +17,7 @@ import { hasRole, hasPermission, hasAtLeastOnePermission } from '../../../../../ import './visitorInfo.html'; import { APIClient } from '../../../../../utils/client'; import { RoomManager } from '../../../../../ui-utils/client'; +import { DateFormat } from '../../../../../lib/client'; const isSubscribedToRoom = () => { const data = Template.currentData(); @@ -62,36 +63,44 @@ Template.visitorInfo.helpers({ return tags && tags.join(', '); }, - customFields() { - const fields = []; - let livechatData = {}; - const user = Template.instance().user.get(); - if (user) { - livechatData = _.extend(livechatData, user.livechatData); + customRoomFields() { + const customFields = Template.instance().customFields.get(); + if (!customFields || customFields.length === 0) { + return []; } - const data = Template.currentData(); - if (data && data.rid) { - const room = Template.instance().room.get(); - if (room) { - livechatData = _.extend(livechatData, room.livechatData); + const fields = []; + const room = Template.instance().room.get(); + const { livechatData = {} } = room || {}; + + Object.keys(livechatData).forEach((key) => { + const field = _.findWhere(customFields, { _id: key }); + if (field && field.visibility !== 'hidden' && field.scope === 'room') { + fields.push({ label: field.label, value: livechatData[key] }); } + }); + + return fields; + }, + + customVisitorFields() { + const customFields = Template.instance().customFields.get(); + if (!customFields || customFields.length === 0) { + return []; } - if (!_.isEmpty(livechatData)) { - for (const _id in livechatData) { - if (livechatData.hasOwnProperty(_id)) { - const customFields = Template.instance().customFields.get(); - if (customFields) { - const field = _.findWhere(customFields, { _id }); - if (field && field.visibility !== 'hidden') { - fields.push({ label: field.label, value: livechatData[_id] }); - } - } - } + const fields = []; + const user = Template.instance().user.get(); + const { livechatData = {} } = user || {}; + + Object.keys(livechatData).forEach((key) => { + const field = _.findWhere(customFields, { _id: key }); + if (field && field.visibility !== 'hidden' && field.scope === 'visitor') { + fields.push({ label: field.label, value: livechatData[key] }); } - return fields; - } + }); + + return fields; }, createdAt() { @@ -190,6 +199,27 @@ Template.visitorInfo.helpers({ canForwardGuest() { return hasPermission('transfer-livechat-guest'); }, + + roomClosedDateTime() { + const { closedAt } = this; + return DateFormat.formatDateAndTime(closedAt); + }, + + roomClosedBy() { + const { closedBy = {}, servedBy = {} } = this; + let { closer } = this; + + if (closer === 'user') { + if (servedBy._id !== closedBy._id) { + return closedBy.username; + } + + closer = 'agent'; + } + + const closerLabel = closer.charAt(0).toUpperCase() + closer.slice(1); + return t(`${ closerLabel }`); + }, }); Template.visitorInfo.events({ diff --git a/app/livechat/client/views/sideNav/livechat.js b/app/livechat/client/views/sideNav/livechat.js index aeb1571fab03a..d3faba2b30922 100644 --- a/app/livechat/client/views/sideNav/livechat.js +++ b/app/livechat/client/views/sideNav/livechat.js @@ -37,20 +37,16 @@ Template.livechat.helpers({ open: true, }; - const user = Users.findOne(Meteor.userId(), { - fields: { 'settings.preferences.sidebarShowUnread': 1 }, - }); + const user = Meteor.userId(); if (getUserPreference(user, 'sidebarShowUnread')) { query.alert = { $ne: true }; } - return ChatSubscription.find(query, { - sort: { - t: 1, - fname: 1, - }, - }); + const sortBy = getUserPreference(user, 'sidebarSortby'); + const sort = sortBy === 'activity' ? { _updatedAt: - 1 } : { fname: 1 }; + + return ChatSubscription.find(query, { sort }); }, inquiries() { diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js index ccaebb403796a..dfff59524816a 100644 --- a/app/livechat/server/api/lib/livechat.js +++ b/app/livechat/server/api/lib/livechat.js @@ -7,8 +7,8 @@ import { Livechat } from '../../lib/Livechat'; import { callbacks } from '../../../../callbacks/server'; import { normalizeAgent } from '../../lib/Helper'; -export function online() { - return Livechat.online(); +export function online(department) { + return Livechat.online(department); } export function findTriggers() { diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js index 4ebdbc345e61d..a1f4bab03405f 100644 --- a/app/livechat/server/api/v1/config.js +++ b/app/livechat/server/api/v1/config.js @@ -8,6 +8,7 @@ API.v1.addRoute('livechat/config', { try { check(this.queryParams, { token: Match.Maybe(String), + department: Match.Maybe(String), }); const config = settings(); @@ -15,9 +16,8 @@ API.v1.addRoute('livechat/config', { return API.v1.success({ config: { enabled: false } }); } - const status = online(); - - const { token } = this.queryParams; + const { token, department } = this.queryParams; + const status = online(department); const guest = token && findGuest(token); let room; diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js index fa3c7b2fd71a6..eb3ccee0feca9 100644 --- a/app/livechat/server/index.js +++ b/app/livechat/server/index.js @@ -80,24 +80,6 @@ import './lib/stream/inquiry'; import './lib/stream/queueManager'; import './sendMessageBySMS'; import './unclosedLivechats'; -import './publications/customFields'; -import './publications/departmentAgents'; -import './publications/externalMessages'; -import './publications/livechatAgents'; -import './publications/livechatAppearance'; -import './publications/livechatDepartments'; -import './publications/livechatIntegration'; -import './publications/livechatManagers'; -import './publications/livechatMonitoring'; -import './publications/livechatRooms'; -import './publications/livechatQueue'; -import './publications/livechatTriggers'; -import './publications/livechatVisitors'; -import './publications/visitorHistory'; -import './publications/visitorInfo'; -import './publications/visitorPageVisited'; -import './publications/livechatInquiries'; -import './publications/livechatOfficeHours'; import './api'; import './api/rest'; diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index de05200e2dafb..f2f43691ce722 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -50,19 +50,19 @@ export const Livechat = { }, }), - online() { + online(department) { if (settings.get('Livechat_accept_chats_with_no_agents')) { return true; } if (settings.get('Livechat_assign_new_conversation_to_bot')) { - const botAgents = Livechat.getBotAgents(); + const botAgents = Livechat.getBotAgents(department); if (botAgents && botAgents.count() > 0) { return true; } } - const onlineAgents = Livechat.getOnlineAgents(); + const onlineAgents = Livechat.getOnlineAgents(department); return (onlineAgents && onlineAgents.count() > 0) || settings.get('Livechat_accept_chats_with_no_agents'); }, @@ -276,7 +276,7 @@ export const Livechat = { return false; }, - saveGuest({ _id, name, email, phone }) { + saveGuest({ _id, name, email, phone, livechatData = {} }) { const updateData = {}; if (name) { @@ -288,6 +288,23 @@ export const Livechat = { if (phone) { updateData.phone = phone; } + + const customFields = {}; + const fields = LivechatCustomField.find({ scope: 'visitor' }); + fields.forEach((field) => { + if (!livechatData.hasOwnProperty(field._id)) { + return; + } + const value = s.trim(livechatData[field._id]); + if (value !== '' && field.regexp !== undefined && field.regexp !== '') { + const regexp = new RegExp(field.regexp); + if (!regexp.test(value)) { + throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: field.label })); + } + } + customFields[field._id] = value; + }); + updateData.livechatData = customFields; const ret = LivechatVisitors.saveGuestById(_id, updateData); Meteor.defer(() => { @@ -338,10 +355,10 @@ export const Livechat = { // Retreive the closed room room = LivechatRooms.findOneByIdOrName(rid); - sendMessage(user, message, room); + sendMessage(user || visitor, message, room); if (servedBy) { - Subscriptions.hideByRoomIdAndUserId(rid, servedBy._id); + Subscriptions.removeByRoomIdAndUserId(rid, servedBy._id); } Messages.createCommandWithRoomIdAndUser('promptTranscript', rid, closeData.closedBy); @@ -364,6 +381,13 @@ export const Livechat = { throw new Meteor.Error('invalid-custom-field'); } + if (customField.regexp !== undefined && customField.regexp !== '') { + const regexp = new RegExp(customField.regexp); + if (!regexp.test(value)) { + throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: key })); + } + } + if (customField.scope === 'room') { return LivechatRooms.updateDataByToken(token, key, value, overwrite); } @@ -414,7 +438,26 @@ export const Livechat = { }, saveRoomInfo(roomData, guestData) { - if ((roomData.topic != null || roomData.tags != null) && !LivechatRooms.setTopicAndTagsById(roomData._id, roomData.topic, roomData.tags)) { + const { livechatData = {} } = roomData; + const customFields = {}; + + const fields = LivechatCustomField.find({ scope: 'room' }); + fields.forEach((field) => { + if (!livechatData.hasOwnProperty(field._id)) { + return; + } + const value = s.trim(livechatData[field._id]); + if (value !== '' && field.regexp !== undefined && field.regexp !== '') { + const regexp = new RegExp(field.regexp); + if (!regexp.test(value)) { + throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: field.label })); + } + } + customFields[field._id] = value; + }); + roomData.livechatData = customFields; + + if (!LivechatRooms.saveRoomById(roomData)) { return false; } diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index e8bdcf138cf60..cdd4aa1c60121 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -9,10 +9,6 @@ import { Livechat } from './Livechat'; export const QueueManager = { async requestRoom({ guest, message, roomInfo, agent }) { - if (!Livechat.online()) { - throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); - } - check(message, Match.ObjectIncluding({ rid: String, })); @@ -23,6 +19,10 @@ export const QueueManager = { department: Match.Maybe(String), })); + if (!Livechat.online(guest.department)) { + throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); + } + const { rid } = message; const name = (roomInfo && roomInfo.fname) || guest.name || guest.username; diff --git a/app/livechat/server/methods/saveCustomField.js b/app/livechat/server/methods/saveCustomField.js index 9f6520b4d5675..4630b967cd264 100644 --- a/app/livechat/server/methods/saveCustomField.js +++ b/app/livechat/server/methods/saveCustomField.js @@ -14,7 +14,7 @@ Meteor.methods({ check(_id, String); } - check(customFieldData, Match.ObjectIncluding({ field: String, label: String, scope: String, visibility: String })); + check(customFieldData, Match.ObjectIncluding({ field: String, label: String, scope: String, visibility: String, regexp: String })); if (!/^[0-9a-zA-Z-_]+$/.test(customFieldData.field)) { throw new Meteor.Error('error-invalid-custom-field-nmae', 'Invalid custom field name. Use only letters, numbers, hyphens and underscores.', { method: 'livechat:saveCustomField' }); @@ -26,7 +26,6 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-custom-field', 'Custom Field Not found', { method: 'livechat:saveCustomField' }); } } - - return LivechatCustomField.createOrUpdateCustomField(_id, customFieldData.field, customFieldData.label, customFieldData.scope, customFieldData.visibility); + return LivechatCustomField.createOrUpdateCustomField(_id, customFieldData.field, customFieldData.label, customFieldData.scope, customFieldData.visibility, { regexp: customFieldData.regexp }); }, }); diff --git a/app/livechat/server/methods/saveInfo.js b/app/livechat/server/methods/saveInfo.js index 8829b90e48b0c..6cfe1150fcae5 100644 --- a/app/livechat/server/methods/saveInfo.js +++ b/app/livechat/server/methods/saveInfo.js @@ -17,12 +17,14 @@ Meteor.methods({ name: Match.Optional(String), email: Match.Optional(String), phone: Match.Optional(String), + livechatData: Match.Optional(Object), })); check(roomData, Match.ObjectIncluding({ _id: String, topic: Match.Optional(String), tags: Match.Optional([String]), + livechatData: Match.Optional(Object), })); const room = LivechatRooms.findOneById(roomData._id, { t: 1, servedBy: 1 }); diff --git a/app/livechat/server/publications/customFields.js b/app/livechat/server/publications/customFields.js deleted file mode 100644 index d5eed528c522f..0000000000000 --- a/app/livechat/server/publications/customFields.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import s from 'underscore.string'; - -import { hasPermission } from '../../../authorization'; -import { LivechatCustomField } from '../../../models'; - -Meteor.publish('livechat:customFields', function(_id) { - console.warn('The publication "livechat:customFields" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:customFields' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:customFields' })); - } - - if (s.trim(_id)) { - return LivechatCustomField.find({ _id }); - } - - return LivechatCustomField.find(); -}); diff --git a/app/livechat/server/publications/departmentAgents.js b/app/livechat/server/publications/departmentAgents.js deleted file mode 100644 index 5b10f8282e332..0000000000000 --- a/app/livechat/server/publications/departmentAgents.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatDepartmentAgents } from '../../../models'; - -Meteor.publish('livechat:departmentAgents', function(departmentId, agentId) { - console.warn('The publication "livechat:departmentAgents" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departmentAgents' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departmentAgents' })); - } - - const filter = { - ...departmentId && { departmentId }, - ...agentId && { agentId }, - }; - - return LivechatDepartmentAgents.find(filter); -}); diff --git a/app/livechat/server/publications/externalMessages.js b/app/livechat/server/publications/externalMessages.js deleted file mode 100644 index c7b5357153b6b..0000000000000 --- a/app/livechat/server/publications/externalMessages.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { LivechatExternalMessage } from '../../../models/server'; - -Meteor.publish('livechat:externalMessages', function(roomId) { - console.warn('The publication "livechat:externalMessages" is deprecated and will be removed after version v3.0.0'); - return LivechatExternalMessage.findByRoomId(roomId); -}); diff --git a/app/livechat/server/publications/livechatAgents.js b/app/livechat/server/publications/livechatAgents.js deleted file mode 100644 index ae9167a601f86..0000000000000 --- a/app/livechat/server/publications/livechatAgents.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission, getUsersInRole } from '../../../authorization'; - -Meteor.publish('livechat:agents', function() { - console.warn('The publication "livechat:agents" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); - } - - if (!hasPermission(this.userId, 'manage-livechat-agents')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); - } - - const self = this; - - const handle = getUsersInRole('livechat-agent').observeChanges({ - added(id, fields) { - self.added('agentUsers', id, fields); - }, - changed(id, fields) { - self.changed('agentUsers', id, fields); - }, - removed(id) { - self.removed('agentUsers', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatAppearance.js b/app/livechat/server/publications/livechatAppearance.js deleted file mode 100644 index c5dd119ecd407..0000000000000 --- a/app/livechat/server/publications/livechatAppearance.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; - -Meteor.publish('livechat:appearance', function() { - console.warn('The publication "livechat:appearance" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:appearance' })); - } - - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:appearance' })); - } - - const query = { - _id: { - $in: [ - 'Livechat_title', - 'Livechat_title_color', - 'Livechat_show_agent_info', - 'Livechat_show_agent_email', - 'Livechat_display_offline_form', - 'Livechat_offline_form_unavailable', - 'Livechat_offline_message', - 'Livechat_offline_success_message', - 'Livechat_offline_title', - 'Livechat_offline_title_color', - 'Livechat_offline_email', - 'Livechat_conversation_finished_message', - 'Livechat_conversation_finished_text', - 'Livechat_registration_form', - 'Livechat_name_field_registration_form', - 'Livechat_email_field_registration_form', - 'Livechat_registration_form_message', - ], - }, - }; - - const self = this; - - const handle = Settings.find(query).observeChanges({ - added(id, fields) { - self.added('livechatAppearance', id, fields); - }, - changed(id, fields) { - self.changed('livechatAppearance', id, fields); - }, - removed(id) { - self.removed('livechatAppearance', id); - }, - }); - - this.ready(); - - this.onStop(() => { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatDepartments.js b/app/livechat/server/publications/livechatDepartments.js deleted file mode 100644 index eb0ea3084c87e..0000000000000 --- a/app/livechat/server/publications/livechatDepartments.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatDepartment } from '../../../models'; - -Meteor.publish('livechat:departments', function(_id, limit = 50) { - console.warn('The publication "livechat:departments" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departments' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departments' })); - } - - if (_id) { - return LivechatDepartment.findByDepartmentId(_id); - } - - return LivechatDepartment.find({}, { limit }); -}); diff --git a/app/livechat/server/publications/livechatInquiries.js b/app/livechat/server/publications/livechatInquiries.js deleted file mode 100644 index cf907a14649db..0000000000000 --- a/app/livechat/server/publications/livechatInquiries.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission, hasRole } from '../../../authorization'; -import { settings } from '../../../settings'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry } from '../../../models/server'; - -Meteor.publish('livechat:inquiry', function(_id) { - console.warn('The publication "livechat:inquiry" is deprecated and will be removed after version v4.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:inquiry' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:inquiry' })); - } - - const publication = this; - const limit = settings.get('Livechat_guest_pool_max_number_incoming_livechats_displayed'); - let departmentIds; - if (!hasRole(this.userId, 'livechat-manager')) { - const departmentAgents = LivechatDepartmentAgents.findByAgentId(this.userId).fetch().map((d) => d.departmentId); - departmentIds = LivechatDepartment.find({ _id: { $in: departmentAgents }, enabled: true }).fetch().map((d) => d._id); - } - - const filter = { - status: 'queued', - ..._id && { _id }, - ...departmentIds && departmentIds.length > 0 && { department: { $in: departmentIds } }, - }; - - const options = { - ...limit && { limit }, - }; - - const cursorHandle = LivechatInquiry.find(filter, options).observeChanges({ - added(_id, record) { - return publication.added('rocketchat_livechat_inquiry', _id, record); - }, - changed(_id, record) { - return publication.changed('rocketchat_livechat_inquiry', _id, record); - }, - removed(_id) { - return publication.removed('rocketchat_livechat_inquiry', _id); - }, - }); - - this.ready(); - return this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatIntegration.js b/app/livechat/server/publications/livechatIntegration.js deleted file mode 100644 index 47b418f90f5bc..0000000000000 --- a/app/livechat/server/publications/livechatIntegration.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; - -Meteor.publish('livechat:integration', function() { - console.warn('The publication "livechat:integration" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:integration' })); - } - - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:integration' })); - } - - const self = this; - - const handle = Settings.findByIds(['Livechat_webhookUrl', 'Livechat_secret_token', 'Livechat_webhook_on_close', 'Livechat_webhook_on_offline_msg', 'Livechat_webhook_on_visitor_message', 'Livechat_webhook_on_agent_message']).observeChanges({ - added(id, fields) { - self.added('livechatIntegration', id, fields); - }, - changed(id, fields) { - self.changed('livechatIntegration', id, fields); - }, - removed(id) { - self.removed('livechatIntegration', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatManagers.js b/app/livechat/server/publications/livechatManagers.js deleted file mode 100644 index 561c9be13c3b4..0000000000000 --- a/app/livechat/server/publications/livechatManagers.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission, getUsersInRole } from '../../../authorization'; - -Meteor.publish('livechat:managers', function() { - console.warn('The publication "livechat:managers" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:managers' })); - } - - if (!hasPermission(this.userId, 'manage-livechat-managers')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:managers' })); - } - - const self = this; - - const handle = getUsersInRole('livechat-manager').observeChanges({ - added(id, fields) { - self.added('managerUsers', id, fields); - }, - changed(id, fields) { - self.changed('managerUsers', id, fields); - }, - removed(id) { - self.removed('managerUsers', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatMonitoring.js b/app/livechat/server/publications/livechatMonitoring.js deleted file mode 100644 index 46cc1422148eb..0000000000000 --- a/app/livechat/server/publications/livechatMonitoring.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; - -Meteor.publish('livechat:monitoring', function(date) { - console.warn('The publication "livechat:monitoring" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:monitoring' })); - } - - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:monitoring' })); - } - - date = { - gte: new Date(date.gte), - lt: new Date(date.lt), - }; - - check(date.gte, Date); - check(date.lt, Date); - - const self = this; - - const handle = LivechatRooms.getAnalyticsMetricsBetweenDate('l', date).observeChanges({ - added(id, fields) { - self.added('livechatMonitoring', id, fields); - }, - changed(id, fields) { - self.changed('livechatMonitoring', id, fields); - }, - removed(id) { - self.removed('livechatMonitoring', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatOfficeHours.js b/app/livechat/server/publications/livechatOfficeHours.js deleted file mode 100644 index 573171aa8f054..0000000000000 --- a/app/livechat/server/publications/livechatOfficeHours.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatOfficeHour } from '../../../models'; - -console.warn('The publication "livechat:officeHour" is deprecated and will be removed after version v3.0.0'); -Meteor.publish('livechat:officeHour', function() { - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:officeHour' })); - } - - return LivechatOfficeHour.find(); -}); diff --git a/app/livechat/server/publications/livechatQueue.js b/app/livechat/server/publications/livechatQueue.js deleted file mode 100644 index acfb3c082cefe..0000000000000 --- a/app/livechat/server/publications/livechatQueue.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatDepartmentAgents } from '../../../models'; - -Meteor.publish('livechat:queue', function() { - console.warn('The publication "livechat:queue" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' })); - } - const self = this; - - const handleDepts = LivechatDepartmentAgents.findUsersInQueue().observeChanges({ - added(id, fields) { - self.added('livechatQueueUser', id, fields); - }, - changed(id, fields) { - self.changed('livechatQueueUser', id, fields); - }, - removed(id) { - self.removed('livechatQueueUser', id); - }, - }); - - this.ready(); - - this.onStop(() => { - handleDepts.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatRooms.js b/app/livechat/server/publications/livechatRooms.js deleted file mode 100644 index 1b14da103c781..0000000000000 --- a/app/livechat/server/publications/livechatRooms.js +++ /dev/null @@ -1,108 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { LivechatDepartment, LivechatRooms } from '../../../models'; -import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; - -const userCanAccessRoom = ({ _id }) => { - if (!_id) { - return; - } - - const room = LivechatRooms.findOneById(_id); - const user = Meteor.user(); - - return canAccessRoom(room, user); -}; - -Meteor.publish('livechat:rooms', function(filter = {}, offset = 0, limit = 20) { - console.warn('The publication "livechat:rooms" is deprecated and will be removed after version v4.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:rooms' })); - } - - if (!hasPermission(this.userId, 'view-livechat-rooms') && !userCanAccessRoom(filter)) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:rooms' })); - } - - check(filter, { - _id: Match.Maybe(String), // room id to filter - name: Match.Maybe(String), // room name to filter - agent: Match.Maybe(String), // agent _id who is serving - status: Match.Maybe(String), // either 'opened' or 'closed' - from: Match.Maybe(Date), - to: Match.Maybe(Date), - department: Match.Maybe(String), // room department - customFields: Match.Maybe(Object), - tags: Match.Maybe(Array), - }); - - const query = {}; - if (filter.name) { - query.fname = new RegExp(filter.name, 'i'); - } - if (filter.agent) { - query['servedBy._id'] = filter.agent; - } - if (filter.status) { - if (filter.status === 'opened') { - query.open = true; - } else { - query.open = { $exists: false }; - } - } - if (filter.from) { - query.ts = { - $gte: filter.from, - }; - } - if (filter.to) { - filter.to.setDate(filter.to.getDate() + 1); - filter.to.setSeconds(filter.to.getSeconds() - 1); - - if (!query.ts) { - query.ts = {}; - } - query.ts.$lte = filter.to; - } - if (filter.department) { - query.departmentId = filter.department; - } - if (filter._id) { - query._id = filter._id; - } - if (filter.customFields) { - for (const key in filter.customFields) { - if (filter.customFields[key]) { - query[`livechatData.${ key }`] = new RegExp(filter.customFields[key], 'i'); - } - } - } - if (filter.tags && filter.tags.length) { - query.tags = { - $in: filter.tags, - }; - } - - const self = this; - const handle = LivechatRooms.findLivechat(query, offset, limit).observeChanges({ - added(id, fields) { - fields = Object.assign(fields, { lookupDepartment: fields.departmentId ? LivechatDepartment.findOneById(fields.departmentId) : {} }); - self.added('livechatRoom', id, fields); - }, - changed(id, fields) { - fields = Object.assign(fields, { lookupDepartment: fields.departmentId ? LivechatDepartment.findOneById(fields.departmentId) : {} }); - self.changed('livechatRoom', id, fields); - }, - removed(id) { - self.removed('livechatRoom', id); - }, - }); - - this.ready(); - - this.onStop(() => { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/livechatTriggers.js b/app/livechat/server/publications/livechatTriggers.js deleted file mode 100644 index 8021698978366..0000000000000 --- a/app/livechat/server/publications/livechatTriggers.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatTrigger } from '../../../models'; - -Meteor.publish('livechat:triggers', function(_id) { - console.warn('The publication "livechat:triggers" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:triggers' })); - } - - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:triggers' })); - } - - if (_id !== undefined) { - return LivechatTrigger.findById(_id); - } - return LivechatTrigger.find(); -}); diff --git a/app/livechat/server/publications/livechatVisitors.js b/app/livechat/server/publications/livechatVisitors.js deleted file mode 100644 index ea94204418400..0000000000000 --- a/app/livechat/server/publications/livechatVisitors.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { LivechatVisitors } from '../../../models'; - -Meteor.publish('livechat:visitors', function(date) { - console.warn('The publication "livechat:visitors" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitors' })); - } - - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitors' })); - } - - date = { - gte: new Date(date.gte), - lt: new Date(date.lt), - }; - - check(date.gte, Date); - check(date.lt, Date); - - const self = this; - - const handle = LivechatVisitors.getVisitorsBetweenDate(date).observeChanges({ - added(id, fields) { - self.added('livechatVisitors', id, fields); - }, - changed(id, fields) { - self.changed('livechatVisitors', id, fields); - }, - removed(id) { - self.removed('livechatVisitors', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); -}); diff --git a/app/livechat/server/publications/visitorHistory.js b/app/livechat/server/publications/visitorHistory.js deleted file mode 100644 index 9f2e9296a15a5..0000000000000 --- a/app/livechat/server/publications/visitorHistory.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms, Subscriptions } from '../../../models'; - -Meteor.publish('livechat:visitorHistory', function({ rid: roomId }) { - console.warn('The publication "livechat:visitorHistory" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' })); - } - - const room = LivechatRooms.findOneById(roomId); - - const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } }); - if (!subscription) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' })); - } - - const self = this; - - if (room && room.v && room.v._id) { - const handle = LivechatRooms.findByVisitorId(room.v._id).observeChanges({ - added(id, fields) { - self.added('visitor_history', id, fields); - }, - changed(id, fields) { - self.changed('visitor_history', id, fields); - }, - removed(id) { - self.removed('visitor_history', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); - } else { - self.ready(); - } -}); diff --git a/app/livechat/server/publications/visitorInfo.js b/app/livechat/server/publications/visitorInfo.js deleted file mode 100644 index 87ec966be0cb8..0000000000000 --- a/app/livechat/server/publications/visitorInfo.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms, LivechatVisitors } from '../../../models'; - -Meteor.publish('livechat:visitorInfo', function({ rid: roomId }) { - console.warn('The publication "livechat:visitorInfo" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorInfo' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorInfo' })); - } - - const room = LivechatRooms.findOneById(roomId); - - if (room && room.v && room.v._id) { - return LivechatVisitors.findById(room.v._id); - } - return this.ready(); -}); diff --git a/app/livechat/server/publications/visitorPageVisited.js b/app/livechat/server/publications/visitorPageVisited.js deleted file mode 100644 index fa2a09ce768d0..0000000000000 --- a/app/livechat/server/publications/visitorPageVisited.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms, Messages } from '../../../models'; - -Meteor.publish('livechat:visitorPageVisited', function({ rid: roomId }) { - console.warn('The publication "livechat:visitorPageVisited" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' })); - } - - if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' })); - } - - const self = this; - const room = LivechatRooms.findOneById(roomId); - - if (room) { - const handle = Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history').observeChanges({ - added(id, fields) { - self.added('visitor_navigation_history', id, fields); - }, - changed(id, fields) { - self.changed('visitor_navigation_history', id, fields); - }, - removed(id) { - self.removed('visitor_navigation_history', id); - }, - }); - - self.ready(); - - self.onStop(function() { - handle.stop(); - }); - } else { - self.ready(); - } -}); diff --git a/app/logger/server/index.js b/app/logger/server/index.js index 7c47b9d018118..06fa09708dbe2 100644 --- a/app/logger/server/index.js +++ b/app/logger/server/index.js @@ -1,4 +1,4 @@ -import './publish.js'; +import './streamer.js'; import { LoggerManager, Logger, SystemLogger } from './server'; export { diff --git a/app/logger/server/publish.js b/app/logger/server/streamer.js similarity index 74% rename from app/logger/server/publish.js rename to app/logger/server/streamer.js index 0478dad3ba373..475c7a183efd0 100644 --- a/app/logger/server/publish.js +++ b/app/logger/server/streamer.js @@ -66,24 +66,3 @@ Meteor.startup(() => { }; StdOut.on('write', handler); }); - -Meteor.publish('stdout', function() { - console.warn('The publication "stdout" is deprecated and will be removed after version v3.0.0'); - if (!this.userId || hasPermission(this.userId, 'view-logs') !== true) { - return this.ready(); - } - const handler = (string, item) => { - this.added('stdout', item.id, { - string: item.string, - ts: item.ts, - }); - }; - - StdOut.queue.forEach((item) => handler('', item)); - - this.ready(); - - this.onStop(() => StdOut.removeListener('write', handler)); - - StdOut.on('write', handler); -}); diff --git a/app/mailer/server/api.js b/app/mailer/server/api.js index afe1908620810..45c7f10030f31 100644 --- a/app/mailer/server/api.js +++ b/app/mailer/server/api.js @@ -4,6 +4,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import s from 'underscore.string'; import juice from 'juice'; +import stripHtml from 'string-strip-html'; import { settings } from '../../settings'; @@ -49,7 +50,13 @@ export const replaceEscaped = (str, data = {}) => replace(str, { return ret; }, {}), }); -export const wrap = (html, data = {}) => replaceEscaped(body.replace('{{body}}', html), data); +export const wrap = (html, data = {}) => { + if (settings.get('email_plain_text_only')) { + return replace(html, data); + } + + return replaceEscaped(body.replace('{{body}}', html), data); +}; export const inlinecss = (html) => juice.inlineContent(html, Settings.get('email_style')); export const getTemplate = (template, fn, escape = true) => { let html = ''; @@ -93,14 +100,23 @@ export const rfcMailPatternWithName = /^(?:.*<)?([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~- export const checkAddressFormat = (from) => rfcMailPatternWithName.test(from); -export const sendNoWrap = ({ to, from, replyTo, subject, html, headers }) => { +export const sendNoWrap = ({ to, from, replyTo, subject, html, text, headers }) => { if (!checkAddressFormat(to)) { return; } - Meteor.defer(() => Email.send({ to, from, replyTo, subject, html, headers })); + + if (!text) { + text = stripHtml(html); + } + + if (settings.get('email_plain_text_only')) { + html = undefined; + } + + Meteor.defer(() => Email.send({ to, from, replyTo, subject, html, text, headers })); }; -export const send = ({ to, from, replyTo, subject, html, data, headers }) => sendNoWrap({ to, from, replyTo, subject: replace(subject, data), html: wrap(html, data), headers }); +export const send = ({ to, from, replyTo, subject, html, text, data, headers }) => sendNoWrap({ to, from, replyTo, subject: replace(subject, data), text: text ? replace(text, data) : stripHtml(replace(html, data)), html: wrap(html, data), headers }); export const checkAddressFormatAndThrow = (from, func) => { if (checkAddressFormat(from)) { diff --git a/app/markdown/lib/hljs.js b/app/markdown/lib/hljs.js new file mode 100644 index 0000000000000..1873a07ed98b8 --- /dev/null +++ b/app/markdown/lib/hljs.js @@ -0,0 +1,371 @@ +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('1c', onec); +hljs.registerLanguage('abnf', abnf); +hljs.registerLanguage('accesslog', accesslog); +hljs.registerLanguage('actionscript', actionscript); +hljs.registerLanguage('ada', ada); +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('clean', clean); +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('css', css); +hljs.registerLanguage('d', d); +hljs.registerLanguage('markdown', markdown); +hljs.registerLanguage('dart', dart); +hljs.registerLanguage('delphi', delphi); +hljs.registerLanguage('diff', diff); +hljs.registerLanguage('django', django); +hljs.registerLanguage('dns', dns); +hljs.registerLanguage('dockerfile', dockerfile); +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('ruby', ruby); +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('go', go); +hljs.registerLanguage('golo', golo); +hljs.registerLanguage('gradle', gradle); +hljs.registerLanguage('groovy', groovy); +hljs.registerLanguage('haml', haml); +hljs.registerLanguage('handlebars', handlebars); +hljs.registerLanguage('haskell', haskell); +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('java', java); +hljs.registerLanguage('javascript', javascript); +hljs.registerLanguage('jboss-cli', jbossCli); +hljs.registerLanguage('json', json); +hljs.registerLanguage('julia', julia); +hljs.registerLanguage('julia-repl', juliaRepl); +hljs.registerLanguage('kotlin', kotlin); +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('lua', lua); +hljs.registerLanguage('makefile', makefile); +hljs.registerLanguage('mathematica', mathematica); +hljs.registerLanguage('matlab', matlab); +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('objectivec', objectivec); +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('php', php); +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('python', python); +hljs.registerLanguage('q', q); +hljs.registerLanguage('qml', qml); +hljs.registerLanguage('r', r); + +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('rust', rust); +hljs.registerLanguage('sas', sas); +hljs.registerLanguage('scala', scala); +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('swift', swift); +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('typescript', typescript); +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); + +export default hljs; diff --git a/app/markdown/lib/parser/marked/marked.js b/app/markdown/lib/parser/marked/marked.js index c9d10189c6f56..0404d20a1c3b1 100644 --- a/app/markdown/lib/parser/marked/marked.js +++ b/app/markdown/lib/parser/marked/marked.js @@ -1,9 +1,9 @@ import { Random } from 'meteor/random'; import _ from 'underscore'; import s from 'underscore.string'; -import hljs from 'highlight.js'; import _marked from 'marked'; +import hljs from '../../hljs'; import { settings } from '../../../../settings'; const renderer = new _marked.Renderer(); diff --git a/app/markdown/lib/parser/original/code.js b/app/markdown/lib/parser/original/code.js index 7a061858055f3..1805dc8f4193c 100644 --- a/app/markdown/lib/parser/original/code.js +++ b/app/markdown/lib/parser/original/code.js @@ -4,7 +4,8 @@ */ import { Random } from 'meteor/random'; import s from 'underscore.string'; -import hljs from 'highlight.js'; + +import hljs from '../../hljs'; const inlinecode = (message) => { // Support `text` diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 9ca2a0e6a7a8e..808eaddf918d1 100644 --- a/app/markdown/tests/client.tests.js +++ b/app/markdown/tests/client.tests.js @@ -214,10 +214,10 @@ const inlinecode = { const code = { '```code```': codeWrapper('code', 'clean'), - '```code': codeWrapper('code\n', 'stylus'), - '```code\n': codeWrapper('code\n', 'stylus'), - '```\ncode\n```': codeWrapper('code\n', 'stylus'), - '```code\n```': codeWrapper('code\n', 'stylus'), + '```code': codeWrapper('code\n', 'clean'), + '```code\n': codeWrapper('code\n', 'clean'), + '```\ncode\n```': codeWrapper('code\n', 'clean'), + '```code\n```': codeWrapper('code\n', 'clean'), '```\ncode```': codeWrapper('code', 'clean'), '```javascript\nvar a = \'log\';\nconsole.log(a);```': codeWrapper('var a = \'log\';\nconsole.log(a);', 'javascript'), '```*code*```': codeWrapper('*code*', 'armasm'), diff --git a/app/mentions-flextab/client/views/mentionsFlexTab.html b/app/mentions-flextab/client/views/mentionsFlexTab.html index c011025c39c1c..4884793a3156b 100644 --- a/app/mentions-flextab/client/views/mentionsFlexTab.html +++ b/app/mentions-flextab/client/views/mentionsFlexTab.html @@ -9,7 +9,7 @@

{{_ "No_mentions_found"}}

{{#if hasMore}} diff --git a/app/mentions-flextab/server/index.js b/app/mentions-flextab/server/index.js deleted file mode 100644 index 65577aad4017a..0000000000000 --- a/app/mentions-flextab/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './publications/mentionedMessages'; diff --git a/app/mentions-flextab/server/publications/mentionedMessages.js b/app/mentions-flextab/server/publications/mentionedMessages.js deleted file mode 100644 index 1fd23b563604d..0000000000000 --- a/app/mentions-flextab/server/publications/mentionedMessages.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Messages } from '../../../models'; - -Meteor.publish('mentionedMessages', function(rid, limit = 50) { - console.warn('The publication "mentionedMessages" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - const publication = this; - const user = Users.findOneById(this.userId); - if (!user) { - return this.ready(); - } - if (!Meteor.call('canAccessRoom', rid, this.userId)) { - return this.ready(); - } - const cursorHandle = Messages.findVisibleByMentionAndRoomId(user.username, rid, { - sort: { - ts: -1, - }, - limit, - }).observeChanges({ - added(_id, record) { - record.mentionedList = true; - return publication.added('rocketchat_mentioned_message', _id, record); - }, - changed(_id, record) { - record.mentionedList = true; - return publication.changed('rocketchat_mentioned_message', _id, record); - }, - removed(_id) { - return publication.removed('rocketchat_mentioned_message', _id); - }, - }); - this.ready(); - return this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/message-attachments/client/messageAttachment.html b/app/message-attachments/client/messageAttachment.html index fd738881090cc..960cbe006e4c1 100644 --- a/app/message-attachments/client/messageAttachment.html +++ b/app/message-attachments/client/messageAttachment.html @@ -106,7 +106,7 @@ {{else}}
-
click to load
+
{{_ "Click_to_load"}}
{{/if}}
@@ -170,6 +170,7 @@ {{/if}} {{#each attachments}} + {{injectMessage . ../msg}} {{injectSettings . ../settings}} {{injectIndex . ../index @index}} {{> messageAttachment}} {{/each}} diff --git a/app/message-attachments/client/messageAttachment.js b/app/message-attachments/client/messageAttachment.js index 3f73306fac014..f6d52cebcf711 100644 --- a/app/message-attachments/client/messageAttachment.js +++ b/app/message-attachments/client/messageAttachment.js @@ -71,11 +71,18 @@ Template.messageAttachment.helpers({ injectSettings(data, settings) { data.settings = settings; }, + injectMessage(data, { rid, _id }) { + data.msg = { _id, rid }; + }, isFile() { return this.type === 'file'; }, isPDF() { - if (this.type === 'file' && this.title_link.endsWith('.pdf') && Template.parentData().msg.file) { + if ( + this.type === 'file' + && this.title_link.endsWith('.pdf') + && Template.parentData().msg.file + ) { this.fileId = Template.parentData().msg.file._id; return true; } diff --git a/app/message-attachments/client/stylesheets/messageAttachments.css b/app/message-attachments/client/stylesheets/messageAttachments.css index b716942634425..2ff918cdf8e89 100644 --- a/app/message-attachments/client/stylesheets/messageAttachments.css +++ b/app/message-attachments/client/stylesheets/messageAttachments.css @@ -130,6 +130,7 @@ html.rtl .attachment { align-items: flex-start; & .attachment-flex-column-grow { + word-break: break-word; flex-grow: 1; } } diff --git a/app/message-pin/client/views/pinnedMessages.html b/app/message-pin/client/views/pinnedMessages.html index c4e7b16b2d0d8..0240c6d9f4894 100644 --- a/app/message-pin/client/views/pinnedMessages.html +++ b/app/message-pin/client/views/pinnedMessages.html @@ -9,7 +9,7 @@

{{_ "No_pinned_messages"}}

diff --git a/app/message-pin/client/views/pinnedMessages.js b/app/message-pin/client/views/pinnedMessages.js index 26d6e9d277a88..fea54f0e2f473 100644 --- a/app/message-pin/client/views/pinnedMessages.js +++ b/app/message-pin/client/views/pinnedMessages.js @@ -18,9 +18,6 @@ Template.pinnedMessages.helpers({ const instance = Template.instance(); return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, - message() { - return _.extend(this, { customClass: 'pinned', actionContext: 'pinned' }); - }, hasMore() { return Template.instance().hasMore.get(); }, diff --git a/app/message-pin/server/index.js b/app/message-pin/server/index.js index e4160692c732d..1046ec80107bc 100644 --- a/app/message-pin/server/index.js +++ b/app/message-pin/server/index.js @@ -1,4 +1,3 @@ import './settings'; import './pinMessage'; -import './publications/pinnedMessages'; import './startup/indexes'; diff --git a/app/message-pin/server/publications/pinnedMessages.js b/app/message-pin/server/publications/pinnedMessages.js deleted file mode 100644 index d2ef8ee0bb430..0000000000000 --- a/app/message-pin/server/publications/pinnedMessages.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Messages } from '../../../models'; - -Meteor.publish('pinnedMessages', function(rid, limit = 50) { - console.warn('The publication "pinnedMessages" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - const publication = this; - - const user = Users.findOneById(this.userId); - if (!user) { - return this.ready(); - } - if (!Meteor.call('canAccessRoom', rid, this.userId)) { - return this.ready(); - } - const cursorHandle = Messages.findPinnedByRoom(rid, { sort: { ts: -1 }, limit }).observeChanges({ - added(_id, record) { - return publication.added('rocketchat_pinned_message', _id, record); - }, - changed(_id, record) { - return publication.changed('rocketchat_pinned_message', _id, record); - }, - removed(_id) { - return publication.removed('rocketchat_pinned_message', _id); - }, - }); - this.ready(); - return this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/message-snippet/client/tabBar/views/snippetedMessages.html b/app/message-snippet/client/tabBar/views/snippetedMessages.html index 539dabf6e0f7f..2c047fbb9d34d 100644 --- a/app/message-snippet/client/tabBar/views/snippetedMessages.html +++ b/app/message-snippet/client/tabBar/views/snippetedMessages.html @@ -12,7 +12,7 @@

{{_ "No_snippet_messages"}}

    {{# with messageContext}} - {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}} + {{#each msg in messages}}{{> message msg=msg room=room subscription=subscription settings=settings u=u}}{{/each}} {{/with}}
{{#if hasMore}} diff --git a/app/message-snippet/client/tabBar/views/snippetedMessages.js b/app/message-snippet/client/tabBar/views/snippetedMessages.js index d28c943259cfe..76ac9b2f422d7 100644 --- a/app/message-snippet/client/tabBar/views/snippetedMessages.js +++ b/app/message-snippet/client/tabBar/views/snippetedMessages.js @@ -18,9 +18,6 @@ Template.snippetedMessages.helpers({ const instance = Template.instance(); return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, - message() { - return _.extend(this, { customClass: 'snippeted', actionContext: 'snippeted' }); - }, hasMore() { return Template.instance().hasMore.get(); }, diff --git a/app/message-snippet/server/index.js b/app/message-snippet/server/index.js index 4e46076a914f8..74e7fa9524c26 100644 --- a/app/message-snippet/server/index.js +++ b/app/message-snippet/server/index.js @@ -1,5 +1,3 @@ import './startup/settings'; import './methods/snippetMessage'; import './requests'; -import './publications/snippetedMessagesByRoom'; -import './publications/snippetedMessage'; diff --git a/app/message-snippet/server/publications/snippetedMessage.js b/app/message-snippet/server/publications/snippetedMessage.js deleted file mode 100644 index b96cc2cd3e0cf..0000000000000 --- a/app/message-snippet/server/publications/snippetedMessage.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Messages, Users, Rooms } from '../../../models'; - -Meteor.publish('snippetedMessage', function(_id) { - console.warn('The publication "snippetedMessage" is deprecated and will be removed after version v3.0.0'); - if (typeof this.userId === 'undefined' || this.userId === null) { - return this.ready(); - } - - const snippet = Messages.findOne({ _id, snippeted: true }); - const user = Users.findOneById(this.userId); - const roomSnippetQuery = { - _id: snippet.rid, - usernames: { - $in: [ - user.username, - ], - }, - }; - - if (!Meteor.call('canAccessRoom', snippet.rid, this.userId)) { - return this.ready(); - } - - if (Rooms.findOne(roomSnippetQuery) === undefined) { - return this.ready(); - } - - const publication = this; - - - if (typeof user === 'undefined' || user === null) { - return this.ready(); - } - - const cursor = Messages.find( - { _id }, - ).observeChanges({ - added(_id, record) { - publication.added('rocketchat_snippeted_message', _id, record); - }, - changed(_id, record) { - publication.changed('rocketchat_snippeted_message', _id, record); - }, - removed(_id) { - publication.removed('rocketchat_snippeted_message', _id); - }, - }); - - this.ready(); - - this.onStop = function() { - cursor.stop(); - }; -}); diff --git a/app/message-snippet/server/publications/snippetedMessagesByRoom.js b/app/message-snippet/server/publications/snippetedMessagesByRoom.js deleted file mode 100644 index 60d8e94aa3831..0000000000000 --- a/app/message-snippet/server/publications/snippetedMessagesByRoom.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Messages } from '../../../models'; - -Meteor.publish('snippetedMessages', function(rid, limit = 50) { - console.warn('The publication "snippetedMessages" is deprecated and will be removed after version v3.0.0'); - if (typeof this.userId === 'undefined' || this.userId === null) { - return this.ready(); - } - - const publication = this; - - const user = Users.findOneById(this.userId); - - if (typeof user === 'undefined' || user === null) { - return this.ready(); - } - - if (!Meteor.call('canAccessRoom', rid, this.userId)) { - return this.ready(); - } - - const cursorHandle = Messages.findSnippetedByRoom( - rid, - { - sort: { ts: -1 }, - limit, - }, - ).observeChanges({ - added(_id, record) { - publication.added('rocketchat_snippeted_message', _id, record); - }, - changed(_id, record) { - publication.changed('rocketchat_snippeted_message', _id, record); - }, - removed(_id) { - publication.removed('rocketchat_snippeted_message', _id); - }, - }); - this.ready(); - - this.onStop = function() { - cursorHandle.stop(); - }; -}); diff --git a/app/message-star/client/views/starredMessages.html b/app/message-star/client/views/starredMessages.html index 387760b8d606c..0b49c3e2d6f37 100644 --- a/app/message-star/client/views/starredMessages.html +++ b/app/message-star/client/views/starredMessages.html @@ -9,7 +9,9 @@

{{_ "No_starred_messages"}}

    {{# with messageContext}} - {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room groupable=false subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}} + {{#each msg in messages}} + {{>message msg=msg room=room groupable=false subscription=subscription settings=settings u=u}} + {{/each}} {{/with}}
{{#if hasMore}} diff --git a/app/message-star/client/views/starredMessages.js b/app/message-star/client/views/starredMessages.js index c346ea8a92f71..7b97d7f63bc74 100644 --- a/app/message-star/client/views/starredMessages.js +++ b/app/message-star/client/views/starredMessages.js @@ -19,9 +19,6 @@ Template.starredMessages.helpers({ const instance = Template.instance(); return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, - message() { - return _.extend(this, { actionContext: 'starred' }); - }, hasMore() { return Template.instance().hasMore.get(); }, diff --git a/app/message-star/server/index.js b/app/message-star/server/index.js index ceeedf7d8669e..d24c04d527885 100644 --- a/app/message-star/server/index.js +++ b/app/message-star/server/index.js @@ -1,4 +1,3 @@ import './settings'; import './starMessage'; -import './publications/starredMessages'; import './startup/indexes'; diff --git a/app/message-star/server/publications/starredMessages.js b/app/message-star/server/publications/starredMessages.js deleted file mode 100644 index cec79cd14d233..0000000000000 --- a/app/message-star/server/publications/starredMessages.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Messages } from '../../../models'; - -Meteor.publish('starredMessages', function(rid, limit = 50) { - console.warn('The publication "starredMessages" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - const publication = this; - const user = Users.findOneById(this.userId); - if (!user) { - return this.ready(); - } - if (!Meteor.call('canAccessRoom', rid, this.userId)) { - return this.ready(); - } - const cursorHandle = Messages.findStarredByUserAtRoom(this.userId, rid, { - sort: { - ts: -1, - }, - limit, - }).observeChanges({ - added(_id, record) { - return publication.added('rocketchat_starred_message', _id, record); - }, - changed(_id, record) { - return publication.changed('rocketchat_starred_message', _id, record); - }, - removed(_id) { - return publication.removed('rocketchat_starred_message', _id); - }, - }); - this.ready(); - return this.onStop(function() { - return cursorHandle.stop(); - }); -}); diff --git a/app/meteor-autocomplete/server/autocomplete-server.js b/app/meteor-autocomplete/server/autocomplete-server.js deleted file mode 100755 index 42be3b2fc2a50..0000000000000 --- a/app/meteor-autocomplete/server/autocomplete-server.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -// This also attaches an onStop callback to sub, so we don't need to worry about that. -// https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js -const Autocomplete = class { - publishCursor(cursor, sub) { - Mongo.Collection._publishCursor(cursor, sub, 'autocompleteRecords'); - } -}; - -Meteor.publish('autocomplete-recordset', function(selector, options, collName) { - console.warn('The publication "autocomplete-recordset" is deprecated and will be removed after version v3.0.0'); - const collection = global[collName]; - - // This is a semi-documented Meteor feature: - // https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js - if (!collection) { - throw new Error(`${ collName } is not defined on the global namespace of the server.`); - } - if (!collection._isInsecure()) { - Meteor._debug(`${ collName } is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.`); - return []; // We need this for the subscription to be marked ready - } - if (options.limit) { - // guard against client-side DOS: hard limit to 50 - options.limit = Math.min(50, Math.abs(options.limit)); - } - - // Push this into our own collection on the client so they don't interfere with other publications of the named collection. - // This also stops the observer automatically when the subscription is stopped. - Autocomplete.publishCursor(collection.find(selector, options), this); - // Mark the subscription ready after the initial addition of documents. - this.ready(); -}); diff --git a/app/meteor-autocomplete/server/index.js b/app/meteor-autocomplete/server/index.js deleted file mode 100644 index 6ababa09bd753..0000000000000 --- a/app/meteor-autocomplete/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './autocomplete-server'; diff --git a/app/models/server/index.js b/app/models/server/index.js index cdbc074bc5bd7..f7bbb89c97a8a 100644 --- a/app/models/server/index.js +++ b/app/models/server/index.js @@ -16,6 +16,7 @@ import Permissions from './models/Permissions'; import Roles from './models/Roles'; import CustomSounds from './models/CustomSounds'; import CustomUserStatus from './models/CustomUserStatus'; +import Imports from './models/Imports'; import Integrations from './models/Integrations'; import IntegrationHistory from './models/IntegrationHistory'; import Invites from './models/Invites'; @@ -65,6 +66,7 @@ export { Roles, CustomSounds, CustomUserStatus, + Imports, Integrations, IntegrationHistory, Invites, diff --git a/app/models/server/models/Imports.js b/app/models/server/models/Imports.js new file mode 100644 index 0000000000000..40eff9147cd8a --- /dev/null +++ b/app/models/server/models/Imports.js @@ -0,0 +1,37 @@ +import { Base } from './_Base'; + +class ImportsModel extends Base { + constructor() { + super('import'); + } + + findLastImport() { + const imports = this.find({}, { sort: { ts: -1 }, limit: 1 }).fetch(); + + if (imports && imports.length) { + return imports.shift(); + } + + return undefined; + } + + invalidateAllOperations() { + this.update({ valid: { $ne: false } }, { $set: { valid: false } }, { multi: true }); + } + + invalidateOperationsExceptId(id) { + this.update({ valid: { $ne: false }, _id: { $ne: id } }, { $set: { valid: false } }, { multi: true }); + } + + invalidateOperationsNotInStatus(status) { + const statusList = [].concat(status); + + this.update({ valid: { $ne: false }, status: { $nin: statusList } }, { $set: { valid: false } }, { multi: true }); + } + + findAllPendingOperations(options = {}) { + return this.find({ valid: true }, options); + } +} + +export default new ImportsModel(); diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 1a2f7543cc747..4a6767d929064 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -68,7 +68,7 @@ export class LivechatRooms extends Base { return this.update(query, update); } - setTopicAndTagsById(_id, topic, tags) { + saveRoomById({ _id, topic, tags, livechatData }) { const setData = {}; const unsetData = {}; @@ -86,6 +86,17 @@ export class LivechatRooms extends Base { unsetData.tags = 1; } + if (livechatData) { + Object.keys(livechatData).forEach((key) => { + const value = s.trim(livechatData[key]); + if (value) { + setData[`livechatData.${ key }`] = value; + } else { + unsetData[`livechatData.${ key }`] = 1; + } + }); + } + const update = {}; if (!_.isEmpty(setData)) { diff --git a/app/models/server/models/LivechatVisitors.js b/app/models/server/models/LivechatVisitors.js index 91e763fac142d..803ecf2dc56fc 100644 --- a/app/models/server/models/LivechatVisitors.js +++ b/app/models/server/models/LivechatVisitors.js @@ -160,6 +160,17 @@ export class LivechatVisitors extends Base { } } + if (data.livechatData) { + Object.keys(data.livechatData).forEach((key) => { + const value = s.trim(data.livechatData[key]); + if (value) { + setData[`livechatData.${ key }`] = value; + } else { + unsetData[`livechatData.${ key }`] = 1; + } + }); + } + const update = {}; if (!_.isEmpty(setData)) { diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js index 99c2d7951d305..b7c1bba880b17 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -126,6 +126,22 @@ export class Messages extends Base { return this.update({ _id: messageId }, { $set: updateObj }); } + setImportFileRocketChatAttachment(importFileId, rocketChatUrl, attachment) { + const query = { + '_importFile.id': importFileId, + }; + + return this.update(query, { + $set: { + '_importFile.rocketChatUrl': rocketChatUrl, + '_importFile.downloaded': true, + }, + $addToSet: { + attachments: attachment, + }, + }, { multi: true }); + } + countVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { const query = { _hidden: { @@ -545,6 +561,9 @@ export class Messages extends Base { username: user.username, }, }, + $unset: { + blocks: 1, + }, }; return this.update(query, update); @@ -715,10 +734,6 @@ export class Messages extends Base { // INSERT createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { - const room = Rooms.findOneById(roomId, { fields: { sysMes: 1 } }); - if ((room != null ? room.sysMes : undefined) === false) { - return; - } const record = { t: type, rid: roomId, @@ -744,10 +759,6 @@ export class Messages extends Base { createNavigationHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) { const type = 'livechat_navigation_history'; - const room = Rooms.findOneById(roomId, { fields: { sysMes: 1 } }); - if ((room != null ? room.sysMes : undefined) === false) { - return; - } const record = { t: type, rid: roomId, @@ -772,10 +783,6 @@ export class Messages extends Base { createTransferHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) { const type = 'livechat_transfer_history'; - const room = Rooms.findOneById(roomId, { fields: { sysMes: 1 } }); - if ((room != null ? room.sysMes : undefined) === false) { - return; - } const record = { t: type, rid: roomId, @@ -1146,6 +1153,25 @@ export class Messages extends Base { return this.findOne(query, { sort: { ts: 1 } }); } + + findAllImportedMessagesWithFilesToDownload() { + const query = { + '_importFile.downloadUrl': { + $exists: true, + }, + '_importFile.rocketChatUrl': { + $exists: false, + }, + '_importFile.downloaded': { + $ne: true, + }, + '_importFile.external': { + $ne: true, + }, + }; + + return this.find(query); + } } export default new Messages(); diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js index e1f6ce73d2771..633a4055df91e 100644 --- a/app/models/server/models/Rooms.js +++ b/app/models/server/models/Rooms.js @@ -218,10 +218,14 @@ export class Rooms extends Base { const query = { _id, }; - const update = { + const update = systemMessages && systemMessages.length > 0 ? { $set: { sysMes: systemMessages, }, + } : { + $unset: { + sysMes: '', + }, }; return this.update(query, update); } diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 5b62a95d9d459..42222ec373887 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -429,6 +429,12 @@ export class Users extends Base { return this.find(query, options); } + findOneByAppId(appId, options) { + const query = { appId }; + + return this.findOne(query, options); + } + findOneByImportId(_id, options) { return this.findOne({ importIds: _id }, options); } @@ -498,6 +504,16 @@ export class Users extends Base { return this.find(query, options); } + findNotOfflineByIds(users, options) { + const query = { + _id: { $in: users }, + status: { + $in: ['online', 'away', 'busy'], + }, + }; + return this.find(query, options); + } + findUsersNotOffline(options) { const query = { username: { @@ -541,7 +557,10 @@ export class Users extends Base { } findActive(options = {}) { - return this.find({ active: true }, options); + return this.find({ + active: true, + type: { $nin: ['app'] }, + }, options); } findActiveByUsernameOrNameRegexWithExceptionsAndConditions(searchTerm, exceptions, conditions, options) { @@ -829,6 +848,16 @@ export class Users extends Base { return this.update(_id, update); } + updateStatusById(_id, status) { + const update = { + $set: { + status, + }, + }; + + return this.update(_id, update); + } + setServiceId(_id, serviceName, serviceId) { const update = { $set: {} }; diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js index 614c196ee7bad..1a65a2c9cebce 100644 --- a/app/models/server/raw/BaseRaw.js +++ b/app/models/server/raw/BaseRaw.js @@ -19,6 +19,10 @@ export class BaseRaw { return this.col.find(...args); } + insert(...args) { + return this.col.insert(...args); + } + update(...args) { return this.col.update(...args); } diff --git a/app/models/server/raw/Statistics.js b/app/models/server/raw/Statistics.js new file mode 100644 index 0000000000000..15b3cf39404a0 --- /dev/null +++ b/app/models/server/raw/Statistics.js @@ -0,0 +1,14 @@ +import { BaseRaw } from './BaseRaw'; + +export class StatisticsRaw extends BaseRaw { + async findLast() { + const options = { + sort: { + createdAt: -1, + }, + limit: 1, + }; + const records = await this.find({}, options).toArray(); + return records && records[0]; + } +} diff --git a/app/models/server/raw/index.js b/app/models/server/raw/index.js index 481fa39a15ab1..042900293a67c 100644 --- a/app/models/server/raw/index.js +++ b/app/models/server/raw/index.js @@ -44,6 +44,8 @@ import CustomUserStatusModel from '../models/CustomUserStatus'; import { CustomUserStatusRaw } from './CustomUserStatus'; import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; +import StatisticsModel from '../models/Statistics'; +import { StatisticsRaw } from './Statistics'; export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection()); export const Roles = new RolesRaw(RolesModel.model.rawCollection()); @@ -68,3 +70,4 @@ export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection()); export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection()); export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection()); export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection()); +export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection()); diff --git a/app/nrr/client/nrr.js b/app/nrr/client/nrr.js index 80feb407d92bf..d783880c2770c 100644 --- a/app/nrr/client/nrr.js +++ b/app/nrr/client/nrr.js @@ -39,6 +39,7 @@ Blaze.renderNonReactive = function(templateName, data) { [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))); view.onViewReady(() => { diff --git a/app/oauth2-server-config/server/admin/publications/oauthApps.js b/app/oauth2-server-config/server/admin/publications/oauthApps.js deleted file mode 100644 index 39b400cc6fb93..0000000000000 --- a/app/oauth2-server-config/server/admin/publications/oauthApps.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization'; -import { OAuthApps } from '../../../../models'; - -Meteor.publish('oauthApps', function() { - console.warn('The publication "oauthApps" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - if (!hasPermission(this.userId, 'manage-oauth-apps')) { - this.error(Meteor.Error('error-not-allowed', 'Not allowed', { publish: 'oauthApps' })); - } - return OAuthApps.find(); -}); diff --git a/app/oauth2-server-config/server/index.js b/app/oauth2-server-config/server/index.js index 4fe04df84ed2b..f7f2e3bb2148d 100644 --- a/app/oauth2-server-config/server/index.js +++ b/app/oauth2-server-config/server/index.js @@ -1,6 +1,5 @@ import './oauth/oauth2-server'; import './oauth/default-services'; -import './admin/publications/oauthApps'; import './admin/methods/addOAuthApp'; import './admin/methods/updateOAuthApp'; import './admin/methods/deleteOAuthApp'; diff --git a/app/oauth2-server-config/server/oauth/oauth2-server.js b/app/oauth2-server-config/server/oauth/oauth2-server.js index e04f5b47d5abc..ff813497bc5a2 100644 --- a/app/oauth2-server-config/server/oauth/oauth2-server.js +++ b/app/oauth2-server-config/server/oauth/oauth2-server.js @@ -46,21 +46,6 @@ oauth2server.routes.get('/oauth/userinfo', function(req, res) { }); }); -Meteor.publish('oauthClient', function(clientId) { - console.warn('The publication "oauthClient" is deprecated and will be removed after version v3.0.0'); - if (!this.userId) { - return this.ready(); - } - return OAuthApps.find({ - clientId, - active: true, - }, { - fields: { - name: 1, - }, - }); -}); - API.v1.addAuthMethod(function() { let headerToken = this.request.headers.authorization; const getToken = this.request.query.access_token; diff --git a/app/oembed/server/server.js b/app/oembed/server/server.js index 0d5a68917da7e..99023e79a37cc 100644 --- a/app/oembed/server/server.js +++ b/app/oembed/server/server.js @@ -65,6 +65,12 @@ const getUrlContent = function(urlObj, redirectCount = 5, callback) { urlObj = URL.parse(urlObj); } + const portsProtocol = { + 80: 'http:', + 8080: 'http:', + 443: 'https:', + }; + const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) { @@ -72,7 +78,12 @@ const getUrlContent = function(urlObj, redirectCount = 5, callback) { } const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; - if (parsedUrl.port && safePorts.length > 0 && !safePorts.includes(parsedUrl.port)) { + + if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { + return callback(); + } + + if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol[port] === parsedUrl.protocol)) { return callback(); } diff --git a/app/search/client/provider/result.html b/app/search/client/provider/result.html index 566738ff0c73f..25f24666be196 100644 --- a/app/search/client/provider/result.html +++ b/app/search/client/provider/result.html @@ -14,7 +14,7 @@
    {{# with messageContext}} - {{#each msg in result.message.docs}}{{#nrr nrrargs 'message' msg=(message msg) room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}} + {{#each msg in result.message.docs}}{{> message msg=(messageParse msg) room=room subscription=subscription settings=settings u=u}}{{/each}} {{/with}}
diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js index 1d62162bd202f..3ab80e13943b0 100644 --- a/app/search/client/provider/result.js +++ b/app/search/client/provider/result.js @@ -99,7 +99,7 @@ Template.DefaultSearchResultTemplate.helpers({ hasMore() { return Template.instance().hasMore.get(); }, - message(msg) { + messageParse(msg) { return { customClass: 'search', actionContext: 'search', ...msg, groupable: false }; }, messageContext, diff --git a/app/settings/server/functions/settings.js b/app/settings/server/functions/settings.js index be6934fb114b7..d1cb239ae8182 100644 --- a/app/settings/server/functions/settings.js +++ b/app/settings/server/functions/settings.js @@ -17,6 +17,45 @@ if (process.env.SETTINGS_HIDDEN) { settings._sorter = {}; +const overrideSetting = (_id, value, options) => { + if (typeof process !== 'undefined' && process.env && process.env[_id]) { + value = process.env[_id]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (options.type === 'int') { + value = parseInt(value); + } + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } else if (Meteor.settings && typeof Meteor.settings[_id] !== 'undefined') { + if (Meteor.settings[_id] == null) { + return false; + } + + value = Meteor.settings[_id]; + options.meteorSettingsValue = value; + options.valueSource = 'meteorSettingsValue'; + } + + if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { + let value = process.env[`OVERWRITE_SETTING_${ _id }`]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (options.type === 'int') { + value = parseInt(value); + } + options.value = value; + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } + + return value; +}; + /* * Add a setting @@ -49,26 +88,6 @@ settings.add = function(_id, value, options = {}) { if (options.i18nDefaultQuery != null) { options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); } - if (typeof process !== 'undefined' && process.env && process.env[_id]) { - value = process.env[_id]; - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (options.type === 'int') { - value = parseInt(value); - } - options.processEnvValue = value; - options.valueSource = 'processEnvValue'; - } else if (Meteor.settings && typeof Meteor.settings[_id] !== 'undefined') { - if (Meteor.settings[_id] == null) { - return false; - } - - value = Meteor.settings[_id]; - options.meteorSettingsValue = value; - options.valueSource = 'meteorSettingsValue'; - } if (options.i18nLabel == null) { options.i18nLabel = _id; } @@ -84,19 +103,9 @@ settings.add = function(_id, value, options = {}) { if (options.autocomplete == null) { options.autocomplete = true; } - if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { - let value = process.env[`OVERWRITE_SETTING_${ _id }`]; - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (options.type === 'int') { - value = parseInt(value); - } - options.value = value; - options.processEnvValue = value; - options.valueSource = 'processEnvValue'; - } + + value = overrideSetting(_id, value, options); + const updateOperations = { $set: options, $setOnInsert: { @@ -125,9 +134,9 @@ settings.add = function(_id, value, options = {}) { $exists: false, }; } - const existantSetting = Settings.db.findOne(query); - if (existantSetting != null) { - if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) { + const existentSetting = Settings.db.findOne(query); + if (existentSetting != null) { + if (existentSetting.editor == null && updateOperations.$setOnInsert.editor != null) { updateOperations.$set.editor = updateOperations.$setOnInsert.editor; delete updateOperations.$setOnInsert.editor; } diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js deleted file mode 100644 index 847995bf23804..0000000000000 --- a/app/statistics/server/functions/get.js +++ /dev/null @@ -1,165 +0,0 @@ -import os from 'os'; - -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { - Sessions, - Settings, - Users, - Rooms, - Subscriptions, - Uploads, - Messages, - LivechatVisitors, - Integrations, -} from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Info, getMongoInfo } from '../../../utils/server'; -import { Migrations } from '../../../migrations/server'; -import { statistics } from '../statisticsNamespace'; -import { Apps } from '../../../apps/server'; -import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; - -const wizardFields = [ - 'Organization_Type', - 'Industry', - 'Size', - 'Country', - 'Language', - 'Server_Type', - 'Register_Server', -]; - -statistics.get = function _getStatistics() { - const statistics = {}; - - // Setup Wizard - statistics.wizard = {}; - wizardFields.forEach((field) => { - const record = Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }); - - // Version - statistics.uniqueId = settings.get('uniqueID'); - if (Settings.findOne('uniqueID')) { - statistics.installedAt = Settings.findOne('uniqueID').createdAt; - } - - if (Info) { - statistics.version = Info.version; - statistics.tag = Info.tag; - statistics.branch = Info.branch; - } - - // User statistics - statistics.totalUsers = Meteor.users.find().count(); - statistics.activeUsers = Meteor.users.find({ active: true }).count(); - statistics.nonActiveUsers = statistics.totalUsers - statistics.activeUsers; - statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count(); - statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count(); - statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; - statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers; - - // Room statistics - statistics.totalRooms = Rooms.find().count(); - statistics.totalChannels = Rooms.findByType('c').count(); - statistics.totalPrivateGroups = Rooms.findByType('p').count(); - statistics.totalDirect = Rooms.findByType('d').count(); - statistics.totalLivechat = Rooms.findByType('l').count(); - statistics.totalDiscussions = Rooms.countDiscussions(); - statistics.totalThreads = Messages.countThreads(); - - // livechat visitors - statistics.totalLivechatVisitors = LivechatVisitors.find().count(); - - // livechat agents - statistics.totalLivechatAgents = Users.findAgents().count(); - - // livechat enabled - statistics.livechatEnabled = settings.get('Livechat_enabled'); - - // Message statistics - statistics.totalMessages = Messages.find().count(); - statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0); - - // Federation statistics - const federationOverviewData = federationGetStatistics(); - - statistics.federatedServers = federationOverviewData.numberOfServers; - statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; - - statistics.lastLogin = Users.getLastLogin(); - statistics.lastMessageSentAt = Messages.getLastTimestamp(); - statistics.lastSeenSubscription = Subscriptions.getLastSeen(); - - statistics.os = { - type: os.type(), - platform: os.platform(), - arch: os.arch(), - release: os.release(), - uptime: os.uptime(), - loadavg: os.loadavg(), - totalmem: os.totalmem(), - freemem: os.freemem(), - cpus: os.cpus(), - }; - - statistics.process = { - nodeVersion: process.version, - pid: process.pid, - uptime: process.uptime(), - }; - - statistics.deploy = { - method: process.env.DEPLOY_METHOD || 'tar', - platform: process.env.DEPLOY_PLATFORM || 'selfinstall', - }; - - statistics.uploadsTotal = Uploads.find().count(); - const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } } }]).toArray()); - statistics.uploadsTotalSize = result ? result.total : 0; - - statistics.migration = Migrations._getControl(); - statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count(); - - const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); - statistics.oplogEnabled = oplogEnabled; - statistics.mongoVersion = mongoVersion; - statistics.mongoStorageEngine = mongoStorageEngine; - - statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); - statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); - statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); - statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); - statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); - statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); - - statistics.apps = { - engineVersion: Info.marketplaceApiVersion, - enabled: Apps.isEnabled(), - totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, - totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, - }; - - const integrations = Integrations.find().fetch(); - - statistics.integrations = { - totalIntegrations: integrations.length, - totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, - totalIncomingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-incoming').length, - totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, - totalOutgoingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-outgoing').length, - totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, - }; - - return statistics; -}; diff --git a/app/statistics/server/functions/getLastStatistics.js b/app/statistics/server/functions/getLastStatistics.js new file mode 100644 index 0000000000000..8231bd8b4b10b --- /dev/null +++ b/app/statistics/server/functions/getLastStatistics.js @@ -0,0 +1,14 @@ +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { statistics } from '../lib/statistics'; +import { Statistics } from '../../../models/server/raw'; + +export async function getLastStatistics({ userId, refresh }) { + if (!await hasPermissionAsync(userId, 'view-statistics')) { + throw new Error('error-not-allowed'); + } + + if (refresh) { + return statistics.save(); + } + return Statistics.findLast(); +} diff --git a/app/statistics/server/functions/getStatistics.js b/app/statistics/server/functions/getStatistics.js new file mode 100644 index 0000000000000..0c801453012a3 --- /dev/null +++ b/app/statistics/server/functions/getStatistics.js @@ -0,0 +1,26 @@ +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Statistics } from '../../../models/server/raw'; + +export async function getStatistics({ userId, query = {}, pagination: { offset, count, sort, fields } }) { + if (!await hasPermissionAsync(userId, 'view-statistics')) { + throw new Error('error-not-allowed'); + } + + const cursor = Statistics.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + const total = await cursor.count(); + + const statistics = await cursor.toArray(); + + return { + statistics, + count: statistics.length, + offset, + total, + }; +} diff --git a/app/statistics/server/functions/save.js b/app/statistics/server/functions/save.js deleted file mode 100644 index 506dd61e3e127..0000000000000 --- a/app/statistics/server/functions/save.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Statistics } from '../../../models'; -import { statistics } from '../statisticsNamespace'; - -statistics.save = function() { - const rcStatistics = statistics.get(); - rcStatistics.createdAt = new Date(); - Statistics.insert(rcStatistics); - return rcStatistics; -}; diff --git a/app/statistics/server/index.js b/app/statistics/server/index.js index 7d5d1f16ea38d..29f45ead81b48 100644 --- a/app/statistics/server/index.js +++ b/app/statistics/server/index.js @@ -1,6 +1,6 @@ -import './functions/get'; -import './functions/save'; import './methods/getStatistics'; import './startup/monitor'; -export { statistics } from './statisticsNamespace'; +export { statistics } from './lib/statistics'; +export { getLastStatistics } from './functions/getLastStatistics'; +export { getStatistics } from './functions/getStatistics'; diff --git a/app/statistics/server/lib/statistics.js b/app/statistics/server/lib/statistics.js new file mode 100644 index 0000000000000..994c6e9c1e1ae --- /dev/null +++ b/app/statistics/server/lib/statistics.js @@ -0,0 +1,174 @@ +import os from 'os'; + +import _ from 'underscore'; +import { Meteor } from 'meteor/meteor'; +import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; + +import { + Sessions, + Settings, + Users, + Rooms, + Subscriptions, + Uploads, + Messages, + LivechatVisitors, + Integrations, + Statistics, +} from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Info, getMongoInfo } from '../../../utils/server'; +import { Migrations } from '../../../migrations/server'; +import { Apps } from '../../../apps/server'; +import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; + +const wizardFields = [ + 'Organization_Type', + 'Industry', + 'Size', + 'Country', + 'Language', + 'Server_Type', + 'Register_Server', +]; + +export const statistics = { + get: function _getStatistics() { + const statistics = {}; + + // Setup Wizard + statistics.wizard = {}; + wizardFields.forEach((field) => { + const record = Settings.findOne(field); + if (record) { + const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); + statistics.wizard[wizardField] = record.value; + } + }); + + // Version + statistics.uniqueId = settings.get('uniqueID'); + if (Settings.findOne('uniqueID')) { + statistics.installedAt = Settings.findOne('uniqueID').createdAt; + } + + if (Info) { + statistics.version = Info.version; + statistics.tag = Info.tag; + statistics.branch = Info.branch; + } + + // User statistics + statistics.totalUsers = Users.find().count(); + statistics.activeUsers = Users.getActiveLocalUserCount(); + statistics.nonActiveUsers = Users.find({ active: false }).count(); + statistics.appUsers = Users.find({ type: 'app' }).count(); + statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count(); + statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count(); + statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; + statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers; + + // Room statistics + statistics.totalRooms = Rooms.find().count(); + statistics.totalChannels = Rooms.findByType('c').count(); + statistics.totalPrivateGroups = Rooms.findByType('p').count(); + statistics.totalDirect = Rooms.findByType('d').count(); + statistics.totalLivechat = Rooms.findByType('l').count(); + statistics.totalDiscussions = Rooms.countDiscussions(); + statistics.totalThreads = Messages.countThreads(); + + // livechat visitors + statistics.totalLivechatVisitors = LivechatVisitors.find().count(); + + // livechat agents + statistics.totalLivechatAgents = Users.findAgents().count(); + + // livechat enabled + statistics.livechatEnabled = settings.get('Livechat_enabled'); + + // Message statistics + statistics.totalMessages = Messages.find().count(); + statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0); + + // Federation statistics + const federationOverviewData = federationGetStatistics(); + + statistics.federatedServers = federationOverviewData.numberOfServers; + statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; + + statistics.lastLogin = Users.getLastLogin(); + statistics.lastMessageSentAt = Messages.getLastTimestamp(); + statistics.lastSeenSubscription = Subscriptions.getLastSeen(); + + statistics.os = { + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + uptime: os.uptime(), + loadavg: os.loadavg(), + totalmem: os.totalmem(), + freemem: os.freemem(), + cpus: os.cpus(), + }; + + statistics.process = { + nodeVersion: process.version, + pid: process.pid, + uptime: process.uptime(), + }; + + statistics.deploy = { + method: process.env.DEPLOY_METHOD || 'tar', + platform: process.env.DEPLOY_PLATFORM || 'selfinstall', + }; + + statistics.uploadsTotal = Uploads.find().count(); + const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } } }]).toArray()); + statistics.uploadsTotalSize = result ? result.total : 0; + + statistics.migration = Migrations._getControl(); + statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count(); + + const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); + statistics.oplogEnabled = oplogEnabled; + statistics.mongoVersion = mongoVersion; + statistics.mongoStorageEngine = mongoStorageEngine; + + statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); + statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); + statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); + statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); + statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); + statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); + + statistics.apps = { + engineVersion: Info.marketplaceApiVersion, + enabled: Apps.isEnabled(), + totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, + totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, + }; + + const integrations = Integrations.find().fetch(); + + statistics.integrations = { + totalIntegrations: integrations.length, + totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, + totalIncomingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-incoming').length, + totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, + totalOutgoingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-outgoing').length, + totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, + }; + + return statistics; + }, + save() { + const rcStatistics = statistics.get(); + rcStatistics.createdAt = new Date(); + Statistics.insert(rcStatistics); + return rcStatistics; + }, +}; diff --git a/app/statistics/server/methods/getStatistics.js b/app/statistics/server/methods/getStatistics.js index e5bfbf6305e3f..3bd17de4a229f 100644 --- a/app/statistics/server/methods/getStatistics.js +++ b/app/statistics/server/methods/getStatistics.js @@ -1,22 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { Statistics } from '../../../models'; -import { statistics } from '../statisticsNamespace'; +import { getLastStatistics } from '../functions/getLastStatistics'; Meteor.methods({ getStatistics(refresh) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' }); } - - if (hasPermission(Meteor.userId(), 'view-statistics') !== true) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getStatistics' }); - } - - if (refresh) { - return statistics.save(); - } - return Statistics.findLast(); + return Promise.await(getLastStatistics({ + userId: Meteor.userId(), + refresh, + })); }, }); diff --git a/app/statistics/server/statisticsNamespace.js b/app/statistics/server/statisticsNamespace.js deleted file mode 100644 index bfd4f992e5899..0000000000000 --- a/app/statistics/server/statisticsNamespace.js +++ /dev/null @@ -1 +0,0 @@ -export const statistics = {}; diff --git a/app/theme/client/imports/components/messages.css b/app/theme/client/imports/components/messages.css index ed417dd3ba09b..ffb10d16430db 100644 --- a/app/theme/client/imports/components/messages.css +++ b/app/theme/client/imports/components/messages.css @@ -44,6 +44,14 @@ } } +.rc-ui-kit { + display: inline-block; + + width: 100%; + + max-width: 400px; +} + .message { & .toggle-hidden { display: none; diff --git a/app/theme/client/imports/components/modal.css b/app/theme/client/imports/components/modal.css index 996791d085bed..50cdb198b0723 100644 --- a/app/theme/client/imports/components/modal.css +++ b/app/theme/client/imports/components/modal.css @@ -7,7 +7,7 @@ height: auto; max-height: 90%; - padding: 1.5rem; + padding: 1rem; animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95); @@ -143,20 +143,13 @@ flex: 0 0 auto; - padding: 16px; + padding: 1rem; - background: #f7f8fa; justify-content: space-between; & > .rc-button { margin: 0; } - - &--empty { - padding: 0; - - background: transparent; - } } } diff --git a/app/theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css index 8be3115b5c271..701478ff68491 100644 --- a/app/theme/client/imports/general/base.css +++ b/app/theme/client/imports/general/base.css @@ -87,6 +87,8 @@ button { display: flex; + overflow: hidden; + flex: 1 1 auto; height: 100%; diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css index a51b0f8431ea7..e40cbc2ffa050 100644 --- a/app/theme/client/imports/general/base_old.css +++ b/app/theme/client/imports/general/base_old.css @@ -157,13 +157,15 @@ top: 0; width: 100%; + margin-top: 0 !important; padding: 2px; border-width: 0 0 1px; } .rc-old .alert { - margin-bottom: 20px; + margin: 1rem 0; + padding: 0.5rem; border-width: 1px; border-radius: var(--border-radius); @@ -2847,6 +2849,8 @@ rc-old select, & .body { transition: opacity 0.3s linear; + word-break: break-word; + opacity: 1; & > h1 { @@ -3472,7 +3476,7 @@ rc-old select, white-space: normal; - & h3 { + & h2 { overflow: hidden; width: 100%; @@ -3498,6 +3502,10 @@ rc-old select, } } + & h3 { + margin: 8px 0; + } + & p { user-select: text; @@ -4399,14 +4407,23 @@ rc-old select, text-align: center; - color: var(--color-blue); - - border: 4px dashed var(--color-blue); background: rgba(255, 255, 255, 0.8); font-size: 42px; align-items: center; justify-content: center; + + &--enabled { + color: var(--color-blue); + + border: 4px dashed var(--color-blue); + } + + &--disabled { + color: var(--color-red); + + border: 4px dashed var(--color-red); + } } &.over .dropzone-overlay { @@ -4843,6 +4860,14 @@ rc-old select, & .user.user-card-message { margin-left: -5px; } + + & .thread-icons--thread { + position: relative; + + left: 0; + + margin: 0 3px; + } } .rc-old .messages-box:not(.compact) .hide-avatars .message.sequential .info { diff --git a/app/threads/client/flextab/thread.html b/app/threads/client/flextab/thread.html index 7bac3b0ba4078..c5378fb860829 100644 --- a/app/threads/client/flextab/thread.html +++ b/app/threads/client/flextab/thread.html @@ -11,8 +11,8 @@

{{> icon block="contextual-bar__header-close-icon" icon="plus"}} -
-
{{_ "Drop_to_upload_file"}}
+
+
{{_ dragAndDropLabel}}
    {{# with messageContext}} diff --git a/app/threads/client/flextab/thread.js b/app/threads/client/flextab/thread.js index 986bb5230974a..cf089f3dea8d7 100644 --- a/app/threads/client/flextab/thread.js +++ b/app/threads/client/flextab/thread.js @@ -9,9 +9,8 @@ import { normalizeThreadMessage, call } from '../../../ui-utils/client'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; import { Messages } from '../../../models'; -import { lazyloadtick } from '../../../lazy-load'; import { fileUpload } from '../../../ui/client/lib/fileUpload'; -import { dropzoneEvents } from '../../../ui/client/views/app/room'; +import { dropzoneEvents, dropzoneHelpers } from '../../../ui/client/views/app/room'; import './thread.html'; const sort = { ts: 1 }; @@ -25,7 +24,6 @@ Template.thread.events({ return close && close(); }, 'scroll .js-scroll-thread': _.throttle(({ currentTarget: e }, i) => { - lazyloadtick(); i.atBottom = e.scrollTop >= e.scrollHeight - e.clientHeight; }, 50), 'load img'() { @@ -39,6 +37,7 @@ Template.thread.events({ }); Template.thread.helpers({ + ...dropzoneHelpers, threadTitle() { return normalizeThreadMessage(Template.currentData().mainMessage); }, diff --git a/app/threads/client/flextab/threads.js b/app/threads/client/flextab/threads.js index 13c8f3231d687..a4fa8d9faee18 100644 --- a/app/threads/client/flextab/threads.js +++ b/app/threads/client/flextab/threads.js @@ -4,7 +4,6 @@ import { Template } from 'meteor/templating'; import { ReactiveDict } from 'meteor/reactive-dict'; import _ from 'underscore'; -import { lazyloadtick } from '../../../lazy-load'; import { call } from '../../../ui-utils'; import { Messages, Subscriptions } from '../../../models'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; @@ -28,7 +27,6 @@ Template.threads.events({ return false; }, 'scroll .js-scroll-threads': _.throttle(({ currentTarget: e }, { incLimit }) => { - lazyloadtick(); if (e.offsetHeight + e.scrollTop <= e.scrollHeight - 50) { incLimit && incLimit(); } diff --git a/app/ui-account/client/accountPreferences.html b/app/ui-account/client/accountPreferences.html index 4e91e24964a91..7263b4a1ca1c7 100644 --- a/app/ui-account/client/accountPreferences.html +++ b/app/ui-account/client/accountPreferences.html @@ -285,14 +285,6 @@

    {{_ "Messages"}}

    {{_ "Sidebar"}}

    -
    - -
    - - -
    -
    -
    diff --git a/app/ui-account/client/accountPreferences.js b/app/ui-account/client/accountPreferences.js index 279a910f0c5fe..7c6c38b8a5f4f 100644 --- a/app/ui-account/client/accountPreferences.js +++ b/app/ui-account/client/accountPreferences.js @@ -184,7 +184,6 @@ Template.accountPreferences.onCreated(function() { data.unreadAlert = JSON.parse($('#unreadAlert').find('input:checked').val()); data.sidebarShowDiscussion = JSON.parse($('#sidebarShowDiscussion').find('input:checked').val()); data.notificationsSoundVolume = parseInt($('#notificationsSoundVolume').val()); - data.roomCounterSidebar = JSON.parse($('#roomCounterSidebar').find('input:checked').val()); data.highlights = _.compact(_.map($('[name=highlights]').val().split(/,|\n/), function(e) { return s.trim(e); })); diff --git a/app/ui-account/client/accountProfile.js b/app/ui-account/client/accountProfile.js index 4fdd494bb250a..913e8a1facc2b 100644 --- a/app/ui-account/client/accountProfile.js +++ b/app/ui-account/client/accountProfile.js @@ -135,10 +135,10 @@ Template.accountProfile.helpers({ return; } } - if (!avatar && user.name === realname && user.username === username && getUserEmailAddress(user) === email === email && (!password || password !== confirmationPassword)) { + if (!avatar && user.name === realname && user.username === username && getUserEmailAddress(user) === email && statusText === user.statusText && !password) { return ret; } - if (!validateEmail(email) || (!validateUsername(username) || usernameAvaliable !== true) || !validateName(realname) || !validateStatusMessage(statusText)) { + if (!validateEmail(email) || !validatePassword(password, confirmationPassword) || (!validateUsername(username) || usernameAvaliable !== true) || !validateName(realname) || !validateStatusMessage(statusText)) { return ret; } }, diff --git a/app/ui-account/client/avatar/avatar.html b/app/ui-account/client/avatar/avatar.html index ee0d3e0bee15d..34f56ae6733c1 100644 --- a/app/ui-account/client/avatar/avatar.html +++ b/app/ui-account/client/avatar/avatar.html @@ -1,9 +1,5 @@ diff --git a/app/ui-admin/server/index.js b/app/ui-admin/server/index.js deleted file mode 100644 index 287e3b80941b7..0000000000000 --- a/app/ui-admin/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './publications/adminRooms'; diff --git a/app/ui-admin/server/publications/adminRooms.js b/app/ui-admin/server/publications/adminRooms.js deleted file mode 100644 index 7e040bb837e37..0000000000000 --- a/app/ui-admin/server/publications/adminRooms.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import s from 'underscore.string'; - -import { hasPermission } from '../../../authorization'; -import { Rooms } from '../../../models/server'; - -Meteor.publish('adminRooms', function(filter, types = [], limit) { - console.warn('The publication "adminRooms" is deprecated and will be removed after version v3.0.0'); - const showTypes = Array.isArray(types) ? types.filter((type) => type !== 'dicussions') : []; - const discussion = types.includes('dicussions'); - - if (!this.userId) { - return this.ready(); - } - - if (hasPermission(this.userId, 'view-room-administration') !== true) { - return this.ready(); - } - - const options = { - fields: { - prid: 1, - fname: 1, - name: 1, - t: 1, - cl: 1, - u: 1, - usernames: 1, - usersCount: 1, - muted: 1, - unmuted: 1, - ro: 1, - default: 1, - topic: 1, - msgs: 1, - archived: 1, - tokenpass: 1, - }, - limit, - sort: { - default: -1, - name: 1, - }, - }; - - const name = s.trim(filter); - - if (name && showTypes.length) { - // CACHE: can we stop using publications here? - return Rooms.findByNameContainingAndTypes(name, showTypes, discussion, options); - } - - if (showTypes.length) { - // CACHE: can we stop using publications here? - return Rooms.findByTypes(showTypes, discussion, options); - } - // CACHE: can we stop using publications here? - return Rooms.findByNameContaining(filter, discussion, options); -}); diff --git a/app/ui-flextab/client/tabs/createInviteLink.js b/app/ui-flextab/client/tabs/createInviteLink.js index c264e33cb94b4..7dca20958f073 100644 --- a/app/ui-flextab/client/tabs/createInviteLink.js +++ b/app/ui-flextab/client/tabs/createInviteLink.js @@ -3,6 +3,7 @@ import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import toastr from 'toastr'; import Clipboard from 'clipboard'; +import { Session } from 'meteor/session'; import { t, APIClient } from '../../../utils'; import { formatDateAndTime } from '../../../lib/client/lib/formatDate'; @@ -93,10 +94,16 @@ Template.createInviteLink.onCreated(function() { this.url = new ReactiveVar(''); this.inviteData = new ReactiveVar(null); - getInviteLink(this, this.data.rid); + const { rid } = this.data; - const clipboard = new Clipboard('.js-copy'); - clipboard.on('success', function() { - toastr.success(TAPi18n.__('Copied')); + getInviteLink(this, rid); + + this.clipboard = new Clipboard('.js-copy'); + this.clipboard.on('success', () => { + Session.get('openedRoom') === rid && toastr.success(TAPi18n.__('Copied')); }); }); + +Template.createInviteLink.onDestroyed(function() { + this.clipboard.destroy(); +}); diff --git a/app/ui-flextab/client/tabs/membersList.html b/app/ui-flextab/client/tabs/membersList.html index 62cbfdc3d186b..ad92bc15ff59c 100644 --- a/app/ui-flextab/client/tabs/membersList.html +++ b/app/ui-flextab/client/tabs/membersList.html @@ -36,7 +36,7 @@
  • {{> avatar username=user.username}}
    -
    + {{# userPresence uid=user._id}}
    {{/userPresence}} {{ignored}} {{displayName}} {{utcOffset}}
    {{> icon user=. block="rc-member-list__menu js-more" icon="menu" }} diff --git a/app/ui-flextab/client/tabs/userInfo.html b/app/ui-flextab/client/tabs/userInfo.html index d097c0796a0d5..c57a711be264a 100644 --- a/app/ui-flextab/client/tabs/userInfo.html +++ b/app/ui-flextab/client/tabs/userInfo.html @@ -31,10 +31,10 @@

    {{_ "User_Info"}}

  • {{#if username}}{{/if}} - {{/userPresence}}