diff --git a/.changeset/afraid-poets-sparkle.md b/.changeset/afraid-poets-sparkle.md deleted file mode 100644 index d9669f73a14eb..0000000000000 --- a/.changeset/afraid-poets-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed Security tab visibility to allow password changes when 2FA/E2E is disabled. diff --git a/.changeset/angry-garlics-visit.md b/.changeset/angry-garlics-visit.md new file mode 100644 index 0000000000000..3a6464698e419 --- /dev/null +++ b/.changeset/angry-garlics-visit.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Improved Retention Policy Warning messages diff --git a/.changeset/breezy-pens-sing.md b/.changeset/breezy-pens-sing.md new file mode 100644 index 0000000000000..0725999ef62b3 --- /dev/null +++ b/.changeset/breezy-pens-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Removed "Unknown media type" errors on the client side by using `application/octet-stream` as a fallback media type (MIME type) for all files diff --git a/.changeset/breezy-starfishes-attack.md b/.changeset/breezy-starfishes-attack.md deleted file mode 100644 index 56bac6f3cebd1..0000000000000 --- a/.changeset/breezy-starfishes-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes error `audio.pause() is not a function` and makes the continuous new room notification (livechat) respect the volume set in user preferences. diff --git a/.changeset/brown-lobsters-join.md b/.changeset/brown-lobsters-join.md new file mode 100644 index 0000000000000..ac0e52c8f829c --- /dev/null +++ b/.changeset/brown-lobsters-join.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Resolved an issue with the room type filter not being reset after navigating between admin sections. diff --git a/.changeset/chilly-glasses-sin.md b/.changeset/chilly-glasses-sin.md deleted file mode 100644 index 3ff7f1ef1c1ff..0000000000000 --- a/.changeset/chilly-glasses-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB diff --git a/.changeset/chilly-toys-hunt.md b/.changeset/chilly-toys-hunt.md new file mode 100644 index 0000000000000..79be3fcfc74c2 --- /dev/null +++ b/.changeset/chilly-toys-hunt.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed "File Upload > Accepted Media Types" setting to allow all type of files uploads diff --git a/.changeset/chilly-walls-knock.md b/.changeset/chilly-walls-knock.md deleted file mode 100644 index 4c183061cbe67..0000000000000 --- a/.changeset/chilly-walls-knock.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed an issue while creating tokens via the special `users.createToken` API was not respecting the maximum login tokens allowed for a user. - -The following endpoint was deprecated and will be removed on version `8.0.0`: - -- `/api/v1/users.createToken` - -The following Meteor method (realtime API) was deprecated and will be removed on version `8.0.0`: - -- `createToken` diff --git a/.changeset/clean-moose-cover.md b/.changeset/clean-moose-cover.md new file mode 100644 index 0000000000000..39e6204ce9b4b --- /dev/null +++ b/.changeset/clean-moose-cover.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team diff --git a/.changeset/cuddly-cycles-nail.md b/.changeset/cuddly-cycles-nail.md new file mode 100644 index 0000000000000..ee49600ee865e --- /dev/null +++ b/.changeset/cuddly-cycles-nail.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduced new elements for apps to select users diff --git a/.changeset/cuddly-maps-peel.md b/.changeset/cuddly-maps-peel.md new file mode 100644 index 0000000000000..1d4d8913ec7c0 --- /dev/null +++ b/.changeset/cuddly-maps-peel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed streams being called when the user is not logged in diff --git a/.changeset/dry-shoes-tap.md b/.changeset/dry-shoes-tap.md new file mode 100644 index 0000000000000..f5abf51c0df0c --- /dev/null +++ b/.changeset/dry-shoes-tap.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the supported versions problem, where in most cases the data chosen was the oldest diff --git a/.changeset/eighty-pans-joke.md b/.changeset/eighty-pans-joke.md deleted file mode 100644 index 83eabdd8f8565..0000000000000 --- a/.changeset/eighty-pans-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Force logout the clients which are actively online, whenever a user resets E2EE keys. diff --git a/.changeset/eighty-wasps-kneel.md b/.changeset/eighty-wasps-kneel.md new file mode 100644 index 0000000000000..d8f297de64c34 --- /dev/null +++ b/.changeset/eighty-wasps-kneel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue with how the UI checked for permissions when deciding if editing or deleting a message by moderators users diff --git a/.changeset/eleven-news-stare.md b/.changeset/eleven-news-stare.md deleted file mode 100644 index 8bf62b7aeafa5..0000000000000 --- a/.changeset/eleven-news-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed issue with login via SAML not redirecting to invite link diff --git a/.changeset/eleven-seas-explain.md b/.changeset/eleven-seas-explain.md deleted file mode 100644 index a41d2fbc625d1..0000000000000 --- a/.changeset/eleven-seas-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue that forces the focus on the last message when interacting by mouse on message list diff --git a/.changeset/fair-peaches-cough.md b/.changeset/fair-peaches-cough.md deleted file mode 100644 index 34f7a319924a4..0000000000000 --- a/.changeset/fair-peaches-cough.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/livechat": patch ---- - -Fixes the livechat client ignoring the `livechat_fileuploads_enabled` setting when uploading files diff --git a/.changeset/famous-scissors-teach.md b/.changeset/famous-scissors-teach.md new file mode 100644 index 0000000000000..05d4cbbf8ea50 --- /dev/null +++ b/.changeset/famous-scissors-teach.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Prevent usage of OTR messages with End-to-end Encryption, both feature shouldn't and can't work together. diff --git a/.changeset/fifty-cups-sort.md b/.changeset/fifty-cups-sort.md deleted file mode 100644 index 389391ef8cc9a..0000000000000 --- a/.changeset/fifty-cups-sort.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor ---- - -Created a new endpoint to get a filtered and paginated list of users. diff --git a/.changeset/fifty-planets-rhyme.md b/.changeset/fifty-planets-rhyme.md deleted file mode 100644 index 3d98a1445aaa7..0000000000000 --- a/.changeset/fifty-planets-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Deprecate muteUserInRoom and unmuteUserInRoom meteor methods diff --git a/.changeset/five-monkeys-applaud.md b/.changeset/five-monkeys-applaud.md new file mode 100644 index 0000000000000..aaa1c0ae367cb --- /dev/null +++ b/.changeset/five-monkeys-applaud.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes not being able to reinstall app after installation failure diff --git a/.changeset/five-shoes-fly.md b/.changeset/five-shoes-fly.md new file mode 100644 index 0000000000000..da462e1508c7f --- /dev/null +++ b/.changeset/five-shoes-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed codeBlock styles in light mode diff --git a/.changeset/flat-socks-act.md b/.changeset/flat-socks-act.md deleted file mode 100644 index 7188779ea7cf1..0000000000000 --- a/.changeset/flat-socks-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed wrong `Business hours` validations between different weeks diff --git a/.changeset/forty-bikes-check.md b/.changeset/forty-bikes-check.md new file mode 100644 index 0000000000000..fdc42a68b0e40 --- /dev/null +++ b/.changeset/forty-bikes-check.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Decrypt pinned encrypted messages in the chat and pinned messages contextual bar. diff --git a/.changeset/forty-ghosts-flow.md b/.changeset/forty-ghosts-flow.md new file mode 100644 index 0000000000000..743110f39e615 --- /dev/null +++ b/.changeset/forty-ghosts-flow.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed "Take it" button behavior disabling it when agent status is set to offline diff --git a/.changeset/four-eyes-sniff.md b/.changeset/four-eyes-sniff.md deleted file mode 100644 index 06c9dac06d057..0000000000000 --- a/.changeset/four-eyes-sniff.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-typings": minor -"@rocket.chat/i18n": minor ---- - -Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings. diff --git a/.changeset/four-onions-camp.md b/.changeset/four-onions-camp.md new file mode 100644 index 0000000000000..8068ac023638f --- /dev/null +++ b/.changeset/four-onions-camp.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +When using `DISABLE_DB_WATCHERS=true` this fixes message updates with URLs that were missing the link preview. diff --git a/.changeset/friendly-months-attack.md b/.changeset/friendly-months-attack.md new file mode 100644 index 0000000000000..90f0e58a9b324 --- /dev/null +++ b/.changeset/friendly-months-attack.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Implement E2EE warning callouts letting users know that encrypted messages can't be searched and auditted on search contextual bar and audit panel. diff --git a/.changeset/fuzzy-readers-bake.md b/.changeset/fuzzy-readers-bake.md new file mode 100644 index 0000000000000..a487096a312e3 --- /dev/null +++ b/.changeset/fuzzy-readers-bake.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues with loading license modules when loading the page while logged out diff --git a/.changeset/gold-flowers-shake.md b/.changeset/gold-flowers-shake.md new file mode 100644 index 0000000000000..26182d785c22f --- /dev/null +++ b/.changeset/gold-flowers-shake.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added E2EE room setup header, with just limited functionality and room actions. diff --git a/.changeset/good-ducks-vanish.md b/.changeset/good-ducks-vanish.md deleted file mode 100644 index 3edfc6baca40d..0000000000000 --- a/.changeset/good-ducks-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Introduces sidebar navigability, allowing users to navigate on sidebar channels through keyboard diff --git a/.changeset/good-ghosts-doubt.md b/.changeset/good-ghosts-doubt.md deleted file mode 100644 index 5f4ed8f5a36dd..0000000000000 --- a/.changeset/good-ghosts-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Introduces a resizable Contextualbar allowing users to change the width just by dragging it diff --git a/.changeset/great-moles-rest.md b/.changeset/great-moles-rest.md deleted file mode 100644 index a615edd7df62f..0000000000000 --- a/.changeset/great-moles-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Encrypt file descriptions in E2EE rooms diff --git a/.changeset/green-camels-repair.md b/.changeset/green-camels-repair.md new file mode 100644 index 0000000000000..58b0f6f1a00cc --- /dev/null +++ b/.changeset/green-camels-repair.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed 2 issues with `QueueInactivityMonitor` callback. This callback was in charge of scheduling the job that would close the inquiry, but it was checking on a property that didn't exist. This caused the callback to early return without scheduling the job, making the feature to not to work. diff --git a/.changeset/green-ways-tie.md b/.changeset/green-ways-tie.md deleted file mode 100644 index 73a334fd32a08..0000000000000 --- a/.changeset/green-ways-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed open expanded view (galery mode) for image attachments sent by livechat widget diff --git a/.changeset/grumpy-games-greet.md b/.changeset/grumpy-games-greet.md new file mode 100644 index 0000000000000..1e7f03658ad37 --- /dev/null +++ b/.changeset/grumpy-games-greet.md @@ -0,0 +1,4 @@ +--- +'@rocket.chat/meteor': patch +--- +Changed streaming logic to prevent hidden system messages from being broadcasted through `stream-room-messages`. diff --git a/.changeset/happy-cameras-mix.md b/.changeset/happy-cameras-mix.md new file mode 100644 index 0000000000000..005d6742a686c --- /dev/null +++ b/.changeset/happy-cameras-mix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue where private encrypted room creation was being forced even when E2EE feature was disabled. diff --git a/.changeset/happy-windows-drum.md b/.changeset/happy-windows-drum.md new file mode 100644 index 0000000000000..3fc0c7ec8c3ae --- /dev/null +++ b/.changeset/happy-windows-drum.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue that allowed saveSettings method to save NaN values on numeric settings. diff --git a/.changeset/healthy-clouds-hide.md b/.changeset/healthy-clouds-hide.md new file mode 100644 index 0000000000000..528a1bf275683 --- /dev/null +++ b/.changeset/healthy-clouds-hide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/gazzodown": patch +--- + +Fixes long katex lines overflowing the message component diff --git a/.changeset/heavy-dolphins-lie.md b/.changeset/heavy-dolphins-lie.md new file mode 100644 index 0000000000000..aac6d0bc8e825 --- /dev/null +++ b/.changeset/heavy-dolphins-lie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue where the modal backdrop is overlapping the options of the `Select` component diff --git a/.changeset/heavy-singers-retire.md b/.changeset/heavy-singers-retire.md deleted file mode 100644 index 60244cd4b5f86..0000000000000 --- a/.changeset/heavy-singers-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue where the last threads list item wasn't displaying properly diff --git a/.changeset/hungry-waves-lick.md b/.changeset/hungry-waves-lick.md new file mode 100644 index 0000000000000..294ece663c32e --- /dev/null +++ b/.changeset/hungry-waves-lick.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed file name being incorrectly sent from the client when uploading assets diff --git a/.changeset/lastmessage-e2ee.md b/.changeset/lastmessage-e2ee.md new file mode 100644 index 0000000000000..b3c8642dcff60 --- /dev/null +++ b/.changeset/lastmessage-e2ee.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixed last message preview in Sidebar for E2E Ecrypted channels diff --git a/.changeset/late-drinks-brake.md b/.changeset/late-drinks-brake.md new file mode 100644 index 0000000000000..7c1ba9ddd7f2b --- /dev/null +++ b/.changeset/late-drinks-brake.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +--- + +Fixed an issue that would not allow the user to dismiss the closeToSeatsLimit banner for old workspaces + diff --git a/.changeset/lemon-schools-double.md b/.changeset/lemon-schools-double.md deleted file mode 100644 index b0f623e8d647f..0000000000000 --- a/.changeset/lemon-schools-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed an issue where mentioning a team would trigger the bot message warning that the team is not a part of the channel diff --git a/.changeset/lovely-trainers-kiss.md b/.changeset/lovely-trainers-kiss.md deleted file mode 100644 index 58d6d5159d91d..0000000000000 --- a/.changeset/lovely-trainers-kiss.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -Convert mute/unmute meteor methods to endpoints diff --git a/.changeset/metal-cats-suffer.md b/.changeset/metal-cats-suffer.md new file mode 100644 index 0000000000000..73acebdca85d9 --- /dev/null +++ b/.changeset/metal-cats-suffer.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Support encrypted files on end-to-end encrypted rooms. diff --git a/.changeset/mighty-oranges-wait.md b/.changeset/mighty-oranges-wait.md new file mode 100644 index 0000000000000..888a013d54ae9 --- /dev/null +++ b/.changeset/mighty-oranges-wait.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Added a "LDAP group validation strategy" setting to LDAP channels and roles sync in order to enable faster syncs diff --git a/.changeset/nasty-windows-reply.md b/.changeset/nasty-windows-reply.md new file mode 100644 index 0000000000000..be62ea5587628 --- /dev/null +++ b/.changeset/nasty-windows-reply.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Moves the quotes to be on top of the message for better readability diff --git a/.changeset/nervous-elephants-jam.md b/.changeset/nervous-elephants-jam.md deleted file mode 100644 index cc74cd85842e7..0000000000000 --- a/.changeset/nervous-elephants-jam.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/model-typings': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -Added a new setting to automatically disable users from LDAP that can no longer be found by the background sync diff --git a/.changeset/nervous-wolves-collect.md b/.changeset/nervous-wolves-collect.md new file mode 100644 index 0000000000000..e32377f541794 --- /dev/null +++ b/.changeset/nervous-wolves-collect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue not allowing users without edit-room-retention-policy permission try to edit the room with the retention policy enabled diff --git a/.changeset/nice-hounds-enjoy.md b/.changeset/nice-hounds-enjoy.md deleted file mode 100644 index 311a29bee54c7..0000000000000 --- a/.changeset/nice-hounds-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/eslint-config': minor ---- - -Adds eslint-plugin-jsx-a11y plugin to eslint react config diff --git a/.changeset/nice-zebras-admire.md b/.changeset/nice-zebras-admire.md new file mode 100644 index 0000000000000..74917c0a08cc6 --- /dev/null +++ b/.changeset/nice-zebras-admire.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Prevent E2EE key reset on startup due to possible race conditions diff --git a/.changeset/nine-houses-reply.md b/.changeset/nine-houses-reply.md deleted file mode 100644 index 29bbe0882a76c..0000000000000 --- a/.changeset/nine-houses-reply.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/livechat": patch ---- - -Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. diff --git a/.changeset/odd-goats-fix.md b/.changeset/odd-goats-fix.md new file mode 100644 index 0000000000000..9178620391be9 --- /dev/null +++ b/.changeset/odd-goats-fix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where settings code mirror is not being displayed correctly in full screen mode diff --git a/.changeset/old-geckos-march.md b/.changeset/old-geckos-march.md deleted file mode 100644 index 188a32d3f5157..0000000000000 --- a/.changeset/old-geckos-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Added the ability to serve .well-known paths directly from Rocket.Chat, if using federation, removing the need for special reverse proxy configuration or another component layer for specific types of reverse proxies / loadbalancers. diff --git a/.changeset/orange-clocks-raise.md b/.changeset/orange-clocks-raise.md new file mode 100644 index 0000000000000..81eac16e2a999 --- /dev/null +++ b/.changeset/orange-clocks-raise.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Async End-to-End Encrypted rooms key distribution process. Users now don't need to be online to get the keys of their subscribed encrypted rooms, the key distribution process is now async and users can recieve keys even when they are not online. diff --git a/.changeset/pink-ants-sing.md b/.changeset/pink-ants-sing.md deleted file mode 100644 index 7b4841a11561e..0000000000000 --- a/.changeset/pink-ants-sing.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. -As UI allowed but changes were not saved, this gave the impression that the function was not working. diff --git a/.changeset/pink-parrots-end.md b/.changeset/pink-parrots-end.md deleted file mode 100644 index 9f1863f6915c0..0000000000000 --- a/.changeset/pink-parrots-end.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor ---- - -Created a new endpoint to resend the welcome email to a given user diff --git a/.changeset/plenty-buses-kneel.md b/.changeset/plenty-buses-kneel.md new file mode 100644 index 0000000000000..5c21bdb0bb697 --- /dev/null +++ b/.changeset/plenty-buses-kneel.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Disable "Reply in direct message", "Copy link" and "Forward message" message menu items for encrypted messages as they don't apply to encrypted messages and also disable apps menu items and show a warning. diff --git a/.changeset/popular-bulldogs-accept.md b/.changeset/popular-bulldogs-accept.md new file mode 100644 index 0000000000000..b18e3382148b7 --- /dev/null +++ b/.changeset/popular-bulldogs-accept.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Disable slash commands in encrypted rooms and show a disabled warning. diff --git a/.changeset/popular-fishes-lay.md b/.changeset/popular-fishes-lay.md deleted file mode 100644 index e709c0e356336..0000000000000 --- a/.changeset/popular-fishes-lay.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -**Fixed settings-related statistics not being updated according to the license.** - -We've identified an issue where certain statistics were not reflecting recent license changes. This resulted in outdated information being reported for workspaces. -This change ensures that all reported statistics are current and consider the workspace license. diff --git a/.changeset/proud-coats-repair.md b/.changeset/proud-coats-repair.md new file mode 100644 index 0000000000000..ba1e16b7e05d6 --- /dev/null +++ b/.changeset/proud-coats-repair.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fix the sorting by last chat in Contact Center table diff --git a/.changeset/proud-experts-taste.md b/.changeset/proud-experts-taste.md deleted file mode 100644 index c6a358d13b20e..0000000000000 --- a/.changeset/proud-experts-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed a language priority issue. It should now respect the following order: User Preference -> Browser Language -> Server Language diff --git a/.changeset/rare-colts-repair.md b/.changeset/rare-colts-repair.md new file mode 100644 index 0000000000000..9011de6ff483c --- /dev/null +++ b/.changeset/rare-colts-repair.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue not rendering the proper error and empty state on users in role table diff --git a/.changeset/rare-dancers-own.md b/.changeset/rare-dancers-own.md new file mode 100644 index 0000000000000..358963661bef5 --- /dev/null +++ b/.changeset/rare-dancers-own.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds the missing `ignoreThreads` param fixing the issue not allowing ignoring threads when overriding retention policy diff --git a/.changeset/real-bobcats-train.md b/.changeset/real-bobcats-train.md new file mode 100644 index 0000000000000..6d51414c9fc4f --- /dev/null +++ b/.changeset/real-bobcats-train.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': patch +'@rocket.chat/meteor': patch +--- + +Don't show Join default channels option on edit user form. diff --git a/.changeset/red-cheetahs-heal.md b/.changeset/red-cheetahs-heal.md new file mode 100644 index 0000000000000..5b9934203da06 --- /dev/null +++ b/.changeset/red-cheetahs-heal.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a cosmetic issue where emoji picker object and symbols category icon are swapped diff --git a/.changeset/rude-llamas-notice.md b/.changeset/rude-llamas-notice.md new file mode 100644 index 0000000000000..90c0ca3bd20ae --- /dev/null +++ b/.changeset/rude-llamas-notice.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +"@rocket.chat/omnichannel-services": patch +--- + +Added a new setting `Restrict files access to users who can access room` that controls file visibility. This new setting allows users that "can access a room" to also download the files that are there. This is specially important for users with livechat manager or monitor roles, or agents that have special permissions to view closed rooms, since this allows them to download files on the conversation even after the conversation is closed. +New setting is disabled by default and it is mutually exclusive with the setting `Restrict file access to room members` since this allows _more_ types of users to download files. diff --git a/.changeset/serious-bottles-tie.md b/.changeset/serious-bottles-tie.md new file mode 100644 index 0000000000000..e12bb94a53106 --- /dev/null +++ b/.changeset/serious-bottles-tie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix user not being set as online when setting "Use REST instead of websocket for Meteor calls" is disabled diff --git a/.changeset/shaggy-yaks-train.md b/.changeset/shaggy-yaks-train.md deleted file mode 100644 index 1dbc97b48228a..0000000000000 --- a/.changeset/shaggy-yaks-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. diff --git a/.changeset/sharp-yaks-turn.md b/.changeset/sharp-yaks-turn.md deleted file mode 100644 index 7c05bf5a3b0ec..0000000000000 --- a/.changeset/sharp-yaks-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed supported versions not being updated in airgapped environments diff --git a/.changeset/short-coins-enjoy.md b/.changeset/short-coins-enjoy.md new file mode 100644 index 0000000000000..d47017030fcb4 --- /dev/null +++ b/.changeset/short-coins-enjoy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue where apps installed via the Marketplace would not be shown in the installed list if the app is unpublished diff --git a/.changeset/shy-eyes-march.md b/.changeset/shy-eyes-march.md new file mode 100644 index 0000000000000..398ab6eb11956 --- /dev/null +++ b/.changeset/shy-eyes-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed `EditRoomInfo` encrypted field placement diff --git a/.changeset/silent-dodos-doubt.md b/.changeset/silent-dodos-doubt.md deleted file mode 100644 index 53859f83cc7ea..0000000000000 --- a/.changeset/silent-dodos-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Added a new notification provider in light of the old FCM API deprecation, now you can choose to use the new provider or the old via the `Push_UseLegacy` setting diff --git a/.changeset/slow-cars-press.md b/.changeset/slow-cars-press.md new file mode 100644 index 0000000000000..de4d08ff52ff7 --- /dev/null +++ b/.changeset/slow-cars-press.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduced a new setting which doesn't allow users to access encrypted rooms until E2EE is configured and also doesn't allow users to send un-encrypted messages in encrypted rooms. + +New room setup for E2EE feature which helps users to setup their E2EE keys and introduced states to E2EE feature. diff --git a/.changeset/slow-cows-dance.md b/.changeset/slow-cows-dance.md deleted file mode 100644 index 67097c860cf69..0000000000000 --- a/.changeset/slow-cows-dance.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/rest-typings": patch ---- - -Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels. -This change is to keep the semantics and conventions of the endpoints diff --git a/.changeset/small-moons-matter.md b/.changeset/small-moons-matter.md deleted file mode 100644 index af18b14ac426a..0000000000000 --- a/.changeset/small-moons-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed custom OAuth roles not synced on the first login (on user creation) diff --git a/.changeset/smart-squids-begin.md b/.changeset/smart-squids-begin.md deleted file mode 100644 index 48f3f460ea7e0..0000000000000 --- a/.changeset/smart-squids-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes the missing space between name and user name on system messages diff --git a/.changeset/smooth-knives-turn.md b/.changeset/smooth-knives-turn.md new file mode 100644 index 0000000000000..3964ecc8481b8 --- /dev/null +++ b/.changeset/smooth-knives-turn.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +--- + +Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + +PS: as a workaround reloading after logout or login in also solves the problem. diff --git a/.changeset/soft-shrimps-beg.md b/.changeset/soft-shrimps-beg.md deleted file mode 100644 index 74bd810a93aa2..0000000000000 --- a/.changeset/soft-shrimps-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -This PR have made enhancements to the select and multiselect inputs related to Omnichannel Departments, now the options properly display the complete department names, ensuring clarity for users and added text wrapping for long department names, enhancing readability and UX. diff --git a/.changeset/spotty-seals-whisper.md b/.changeset/spotty-seals-whisper.md new file mode 100644 index 0000000000000..242b5f6dde636 --- /dev/null +++ b/.changeset/spotty-seals-whisper.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/uikit-playground': minor +'@rocket.chat/meteor': minor +--- + +Upgrades fuselage-toastbar version in order to add RTL support to the component diff --git a/.changeset/strange-comics-camp.md b/.changeset/strange-comics-camp.md deleted file mode 100644 index 667ba409a7f30..0000000000000 --- a/.changeset/strange-comics-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where an endpoint was called before checking configuration that enables automatic translation when launching the application diff --git a/.changeset/strange-countries-visit.md b/.changeset/strange-countries-visit.md deleted file mode 100644 index 211dbb2f5a727..0000000000000 --- a/.changeset/strange-countries-visit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-services": patch ---- - -Fixed a problem that caused OTR Session messages' to not being transmitted from one peer to another when running Rocket.Chat as microservices. This was caused by a legacy streamer that tried to use the websocket directly, which works on monolith but doesn't on microservices, cause these events are routed through DDP Streamer service. diff --git a/.changeset/strange-rivers-live.md b/.changeset/strange-rivers-live.md deleted file mode 100644 index b1ebd05c284dc..0000000000000 --- a/.changeset/strange-rivers-live.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@rocket.chat/core-typings': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. -This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. diff --git a/.changeset/strong-bananas-flash.md b/.changeset/strong-bananas-flash.md deleted file mode 100644 index d41697836d117..0000000000000 --- a/.changeset/strong-bananas-flash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button diff --git a/.changeset/sweet-books-trade.md b/.changeset/sweet-books-trade.md deleted file mode 100644 index be828d662f322..0000000000000 --- a/.changeset/sweet-books-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed search room not showing the new name room name changes diff --git a/.changeset/sweet-kiwis-scream.md b/.changeset/sweet-kiwis-scream.md new file mode 100644 index 0000000000000..95a094f2ac402 --- /dev/null +++ b/.changeset/sweet-kiwis-scream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Clicking on a message attachment link in the Desktop App will now initiate a direct download of the attachment only when the attachment is not a PDF file diff --git a/.changeset/swift-readers-speak.md b/.changeset/swift-readers-speak.md deleted file mode 100644 index 25a109d492d6c..0000000000000 --- a/.changeset/swift-readers-speak.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor ---- - -Added "Enable Users" option under "Sync User Active State" LDAP setting to allow only re-enabling users found on LDAP background sync diff --git a/.changeset/tame-ducks-turn.md b/.changeset/tame-ducks-turn.md deleted file mode 100644 index 0ad730b9b3100..0000000000000 --- a/.changeset/tame-ducks-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed a problem that caused `afterCreateUser` callback to be called without new user's roles inside. This caused Omnichannel Business Hour manager to ignore these users from assigning open business hours until the manager restarted or the business hour restarted. diff --git a/.changeset/tame-weeks-shout.md b/.changeset/tame-weeks-shout.md new file mode 100644 index 0000000000000..72bfc864274ff --- /dev/null +++ b/.changeset/tame-weeks-shout.md @@ -0,0 +1,41 @@ +--- +'@rocket.chat/omnichannel-services': minor +'rocketchat-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/web-ui-registration': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/uikit-playground': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/fuselage-ui-kit': minor +'@rocket.chat/instance-status': minor +'@rocket.chat/account-service': minor +'@rocket.chat/mock-providers': minor +'@rocket.chat/api-client': minor +'@rocket.chat/ddp-client': minor +'@rocket.chat/pdf-worker': minor +'@rocket.chat/ui-theming': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/ui-video-conf': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/ui-composer': minor +'@rocket.chat/ui-contexts': minor +'@rocket.chat/license': minor +'@rocket.chat/gazzodown': minor +'@rocket.chat/ui-avatar': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/livechat': minor +'@rocket.chat/models': minor +'@rocket.chat/ui-kit': minor +'@rocket.chat/apps': minor +'@rocket.chat/cron': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +New runtime for apps in the Apps-Engine based on the Deno platform diff --git a/.changeset/ten-stingrays-eat.md b/.changeset/ten-stingrays-eat.md new file mode 100644 index 0000000000000..8a8988bd77c2d --- /dev/null +++ b/.changeset/ten-stingrays-eat.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/i18n": minor +--- + +Added the possibility to choose the time unit (days, hours, minutes) to the global retention policy settings diff --git a/.changeset/thin-peaches-own.md b/.changeset/thin-peaches-own.md deleted file mode 100644 index b002e2f774c24..0000000000000 --- a/.changeset/thin-peaches-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue where message reactions are vertically misaligned when zooming out diff --git a/.changeset/thin-suns-invent.md b/.changeset/thin-suns-invent.md new file mode 100644 index 0000000000000..945f44420797d --- /dev/null +++ b/.changeset/thin-suns-invent.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues causing nonstop sound notification when taking a chat from the `Current Chats` view diff --git a/.changeset/thirty-hotels-greet.md b/.changeset/thirty-hotels-greet.md deleted file mode 100644 index ac226b11b6b66..0000000000000 --- a/.changeset/thirty-hotels-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Redesign Save E2EE password modal diff --git a/.changeset/three-squids-brake.md b/.changeset/three-squids-brake.md new file mode 100644 index 0000000000000..89ed21f8048c8 --- /dev/null +++ b/.changeset/three-squids-brake.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Encrypted thread main message reactivity issues. Earlier the encrypted thread main message was having some reactivity issues and flaky behavior. diff --git a/.changeset/tidy-apes-fry.md b/.changeset/tidy-apes-fry.md new file mode 100644 index 0000000000000..ee3922fa350c2 --- /dev/null +++ b/.changeset/tidy-apes-fry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed inverted navigation direction in the image gallery diff --git a/.changeset/tough-boats-beg.md b/.changeset/tough-boats-beg.md deleted file mode 100644 index bc77048ffbec0..0000000000000 --- a/.changeset/tough-boats-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Support Message Custom Fields on upload API via field `customField` and JSON value diff --git a/.changeset/twelve-seas-battle.md b/.changeset/twelve-seas-battle.md deleted file mode 100644 index a527a93f6212f..0000000000000 --- a/.changeset/twelve-seas-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where old exports would get overwritten by new ones if generated on the same day, when using external storage services (such as Amazon S3) diff --git a/.changeset/two-suns-marry.md b/.changeset/two-suns-marry.md deleted file mode 100644 index 3eae6383a62f4..0000000000000 --- a/.changeset/two-suns-marry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -feat: `ConnectionStatusBar` redesign diff --git a/.changeset/weak-books-tell.md b/.changeset/weak-books-tell.md new file mode 100644 index 0000000000000..675901263f31d --- /dev/null +++ b/.changeset/weak-books-tell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Forces the highlight code language registration, preventing it to not being available when trying to use on the UI diff --git a/.changeset/wild-carrots-know.md b/.changeset/wild-carrots-know.md new file mode 100644 index 0000000000000..6403b5fd0bfd5 --- /dev/null +++ b/.changeset/wild-carrots-know.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Remove password change reason when the `request password change` option is set to false diff --git a/.changeset/wild-keys-obey.md b/.changeset/wild-keys-obey.md deleted file mode 100644 index 9de92ee5671bc..0000000000000 --- a/.changeset/wild-keys-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -Fixes issue causing a desync in different browser windows when a chat is closed and started again diff --git a/.changeset/young-candles-explode.md b/.changeset/young-candles-explode.md deleted file mode 100644 index 91ff2458a4031..0000000000000 --- a/.changeset/young-candles-explode.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/ui-contexts': minor -'@rocket.chat/meteor': minor ---- - -Fixed an issue affecting the update modal/contextual bar by apps when it comes to error handling and regular surface update diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ad64fe9cb966..a834776aeff5c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,12 +22,12 @@ /apps/meteor/server/models @RocketChat/Architecture apps/meteor/server/startup/migrations @RocketChat/Architecture /apps/meteor/packages/rocketchat-livechat @RocketChat/omnichannel -/apps/meteor/server/services/voip @RocketChat/omnichannel +/apps/meteor/server/services/voip-asterisk @RocketChat/omnichannel /apps/meteor/server/services/omnichannel-voip @RocketChat/omnichannel /apps/meteor/server/features/EmailInbox @RocketChat/omnichannel /apps/meteor/ee/app/canned-responses @RocketChat/omnichannel /apps/meteor/ee/app/livechat @RocketChat/omnichannel /apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel -/apps/meteor/ee/client/omnichannel @RocketChat/omnichannel +/apps/meteor/client/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/voip @RocketChat/omnichannel diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 0e921e81f1f33..caa3c63e00f04 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -10,6 +10,10 @@ inputs: install: required: false type: boolean + deno-dir: + required: false + type: string + default: ~/.deno-cache outputs: node-version: @@ -19,6 +23,9 @@ runs: using: composite steps: + - run: echo 'DENO_DIR=${{ inputs.deno-dir }}' >> $GITHUB_ENV + shell: bash + - name: Cache Node Modules if: inputs.cache-modules id: cache-node-modules @@ -26,6 +33,7 @@ runs: with: path: | node_modules + ${{ env.DENO_DIR }} apps/meteor/node_modules apps/meteor/ee/server/services/node_modules key: node-modules-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index a7a6f3f367e25..920aea0aa3087 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -50,6 +50,10 @@ on: type: required: true type: string + db-watcher-disabled: + default: 'false' + required: false + type: string secrets: CR_USER: required: true @@ -63,6 +67,8 @@ on: required: false CODECOV_TOKEN: required: false + REPORTER_JIRA_ROCKETCHAT_API_KEY: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -83,9 +89,16 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }} (${{ matrix.shard }}/${{ inputs.total-shard }})${{ matrix.mongodb-version == '6.0' && ' - Alpine' || '' }} + name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }})${{ matrix.mongodb-version == '6.0' && ' - Alpine' || '' }} steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false + - name: Login to GitHub Container Registry if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') uses: docker/login-action@v2 @@ -161,6 +174,7 @@ jobs: TRANSPORTER: ${{ inputs.transporter }} COVERAGE_DIR: '/tmp/coverage' COVERAGE_REPORTER: 'lcov' + DISABLE_DB_WATCHERS: ${{ inputs.db-watcher-disabled }} run: | docker compose -f docker-compose-ci.yml up -d @@ -238,10 +252,15 @@ jobs: IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} + REPORTER_ROCKETCHAT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPORTER_ROCKETCHAT_AUTHOR: ${{ github.event.pull_request.user.login }} + REPORTER_ROCKETCHAT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORTER_ROCKETCHAT_PR: ${{ github.event.pull_request.number }} QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} QASE_REPORT: ${{ github.ref == 'refs/heads/develop' && 'true' || '' }} CI: true diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 066cc2e3773ea..cf28bbdfc01a1 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -21,6 +21,12 @@ jobs: name: Unit Tests steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - uses: actions/checkout@v4 - name: Setup NodeJS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51e034505a857..ce18d17b8b80a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" @@ -192,6 +198,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" @@ -337,6 +349,7 @@ jobs: QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} test-api-ee: name: 🔨 Test API (EE) @@ -388,11 +401,43 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + + test-ui-ee-no-watcher: + name: 🔨 Test UI (EE) + needs: [checks, build-gh-docker-coverage, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: ui + release: ee + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + shard: '[1, 2, 3, 4, 5]' + total-shard: 5 + mongodb-version: "['6.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} + rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-alpine: ${{ needs.release-versions.outputs.rc-dockerfile-alpine }} + rc-docker-tag-alpine: ${{ needs.release-versions.outputs.rc-docker-tag-alpine }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} + db-watcher-disabled: 'true' + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} + REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} + REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} tests-done: name: ✅ Tests Done runs-on: ubuntu-20.04 - needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee] + needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-ui-ee-no-watcher] steps: - name: Test finish aggregation @@ -599,7 +644,7 @@ jobs: strategy: matrix: - service: ['account', 'authorization', 'ddp-streamer', 'presence', 'stream-hub'] + service: ['account', 'authorization', 'ddp-streamer', 'omnichannel-transcript', 'presence', 'queue-worker', 'stream-hub'] steps: - name: Login to DockerHub diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index 356ac10c97593..bc9d1f042d58a 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -12,6 +12,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.3.7 + - uses: thehanimo/pr-title-checker@v1.4.1 with: GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b074212964ebc..0ee119fe43aa8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,8 +11,8 @@ jobs: steps: - uses: actions/stale@v5 with: - days-before-issue-stale: 10 - days-before-issue-close: 4 + days-before-issue-stale: 14 + days-before-issue-close: 14 any-of-labels: 'stat: need more info,stat: waiting response' stale-issue-label: "stat: no response" stale-issue-message: "This issue has been marked as stale because there has been no further activity in the last 10 days. If the issue remains stale for the next 4 days (a total of 14 days with no activity), then it will be assumed that the question has been resolved and the issue will be automatically closed." diff --git a/.vscode/settings.json b/.vscode/settings.json index 4eaf1836d1fde..2dcd055310d14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.words": [ "autotranslate", + "ciphertext", "Contextualbar", "fname", "Gazzodown", diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 456ed4becafd9..1e9ed3f5e5922 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -13,12 +13,24 @@ RUN groupadd -g 65533 -r rocketchat \ # --chown requires Docker 17.12 and works only on Linux ADD --chown=rocketchat:rocketchat . /app +# needs a mongoinstance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN aptMark="$(apt-mark showmanual)" \ && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ && cd /app/bundle/programs/server \ && npm install \ - && cd npm/node_modules/isolated-vm \ - && npm install \ + && cd npm/node_modules/isolated-vm \ + && npm install \ + && cd /app/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && ../../../deno-bin/bin/deno cache main.ts \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ @@ -37,15 +49,6 @@ VOLUME /app/uploads WORKDIR /app/bundle -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - EXPOSE 3000 CMD ["node", "main.js"] diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 94baef8092174..feebf76a03e70 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,13 +1,68 @@ FROM node:14.21.3-alpine3.16 -RUN apk add --no-cache ttf-dejavu +ENV LANG=C.UTF-8 + +# Installing glibc deps required by Deno +# This replaces libc6-compat +# Copied from https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc, which denoland/deno:alpine-1.37.1 uses +# NOTE: Glibc 2.35 package is broken: https://github.com/sgerrand/alpine-pkg-glibc/issues/176, so we stick to 2.34 for now +RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \ + ALPINE_GLIBC_PACKAGE_VERSION="2.34-r0" && \ + ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ + echo \ + "-----BEGIN PUBLIC KEY-----\ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ + y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\ + tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\ + m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\ + KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\ + Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\ + 1QIDAQAB\ + -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \ + wget \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + mv /etc/nsswitch.conf /etc/nsswitch.conf.bak && \ + apk add --no-cache --force-overwrite \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + \ + mv /etc/nsswitch.conf.bak /etc/nsswitch.conf && \ + rm "/etc/apk/keys/sgerrand.rsa.pub" && \ + (/usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true) && \ + echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \ + \ + apk del glibc-i18n && \ + \ + rm "/root/.wget-hsts" && \ + apk del .build-dependencies && \ + rm \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + apk add --no-cache ttf-dejavu ADD . /app LABEL maintainer="buildmaster@rocket.chat" +# needs a mongo instance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN set -x \ - && apk add --no-cache --virtual .fetch-deps python3 make g++ libc6-compat \ + && apk add --no-cache --virtual .fetch-deps python3 make g++ \ && cd /app/bundle/programs/server \ && npm install --production \ # Start hack for sharp... @@ -20,20 +75,14 @@ RUN set -x \ && npm install isolated-vm@4.4.2 \ && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # End hack for isolated-vm - && cd npm \ + # Cache Deno dependencies for Apps-Engine + && cd npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && /app/bundle/programs/server/npm/node_modules/deno-bin/bin/deno cache main.ts \ + && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ && apk del .fetch-deps -# needs a mongo instance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - VOLUME /app/uploads WORKDIR /app/bundle diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 8107c249add2a..307b0d89eb0dc 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -15,12 +15,12 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 @@ -29,20 +29,20 @@ google-oauth@1.4.4 oauth@2.2.1 oauth2@1.3.2 -check@1.3.2 +check@1.4.1 ddp-rate-limiter@1.2.1 rate-limit@1.1.1 -email@2.2.5 +email@2.2.6 http@2.0.0 meteor-base@1.5.1 -ddp-common@1.4.0 +ddp-common@1.4.1 webapp@1.13.8 -mongo@1.16.8 +mongo@1.16.10 reload@1.3.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 shell-server@0.5.0 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 966586ce54fe9..5152abe9d5821 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.15 +METEOR@2.16 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index a4483a5cf40e1..416ae456f05bd 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,9 +1,9 @@ -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 allow-deny@1.1.1 @@ -15,14 +15,14 @@ binary-heap@1.0.11 boilerplate-generator@1.7.2 caching-compiler@1.2.2 callback-hook@1.5.1 -check@1.3.2 +check@1.4.1 coffeescript@2.7.0 coffeescript-compiler@2.4.1 ddp@1.4.1 -ddp-client@2.6.1 -ddp-common@1.4.0 +ddp-client@2.6.2 +ddp-common@1.4.1 ddp-rate-limiter@1.2.1 -ddp-server@2.7.0 +ddp-server@2.7.1 diff-sequence@1.1.2 dispatch:run-as-user@1.1.1 dynamic-import@0.7.3 @@ -31,7 +31,7 @@ ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 -email@2.2.5 +email@2.2.6 es5-shim@4.8.0 facebook-oauth@1.11.3 facts-base@1.0.1 @@ -45,17 +45,17 @@ id-map@1.1.1 inter-process-messaging@0.1.1 kadira:flow-router@2.12.1 localstorage@1.2.0 -logging@1.3.3 +logging@1.3.4 meteor@1.11.5 meteor-base@1.5.1 meteor-developer-oauth@1.3.2 meteorhacks:inject-initial@1.0.5 minifier-css@1.6.4 -minimongo@1.9.3 +minimongo@1.9.4 modern-browsers@0.1.10 modules@0.20.0 modules-runtime@0.13.1 -mongo@1.16.8 +mongo@1.16.10 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 @@ -84,7 +84,7 @@ rocketchat:streamer@1.1.0 rocketchat:user-presence@2.6.3 rocketchat:version@1.0.0 routepolicy@1.1.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 sha@1.0.9 shell-server@0.5.0 @@ -93,10 +93,10 @@ standard-minifier-css@1.9.2 tracker@1.3.3 twitter-oauth@1.3.3 typescript@4.9.5 -underscore@1.6.0 +underscore@1.6.1 url@1.3.2 webapp@1.13.8 webapp-hashing@1.1.1 zodern:caching-minifier@0.5.0 zodern:standard-minifier-js@5.3.1 -zodern:types@1.0.11 +zodern:types@1.0.13 diff --git a/apps/meteor/.storybook/main.js b/apps/meteor/.storybook/main.js index 0e0b6db7c0e98..d70d3c5d7cc38 100644 --- a/apps/meteor/.storybook/main.js +++ b/apps/meteor/.storybook/main.js @@ -7,7 +7,6 @@ module.exports = { '../client/**/*.stories.{js,tsx}', '../app/**/*.stories.{js,tsx}', '../ee/app/**/*.stories.{js,tsx}', - '../ee/client/**/*.stories.{js,tsx}', ], addons: [ '@storybook/addon-essentials', diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index f44de4f5450ff..568551d83efc6 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,800 @@ # @rocket.chat/meteor +## 6.9.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32621](https://github.com/RocketChat/Rocket.Chat/pull/32621) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes the supported versions problem, where in most cases the data chosen was the oldest + +- ([#32622](https://github.com/RocketChat/Rocket.Chat/pull/32622) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes the issue not allowing users without edit-room-retention-policy permission try to edit the room with the retention policy enabled + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.9.2 + - @rocket.chat/rest-typings@6.9.2 + - @rocket.chat/api-client@0.1.35 + - @rocket.chat/license@0.1.17 + - @rocket.chat/omnichannel-services@0.1.17 + - @rocket.chat/pdf-worker@0.0.41 + - @rocket.chat/presence@0.1.17 + - @rocket.chat/apps@0.0.8 + - @rocket.chat/core-services@0.3.17 + - @rocket.chat/cron@0.0.37 + - @rocket.chat/fuselage-ui-kit@7.0.2 + - @rocket.chat/gazzodown@7.0.2 + - @rocket.chat/model-typings@0.4.3 + - @rocket.chat/ui-contexts@7.0.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.41 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.2 + - @rocket.chat/ui-client@7.0.2 + - @rocket.chat/ui-video-conf@7.0.2 + - @rocket.chat/web-ui-registration@7.0.2 + - @rocket.chat/instance-status@0.0.41 +
+ +## 6.9.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32591](https://github.com/RocketChat/Rocket.Chat/pull/32591) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes issues with loading license modules when loading the page while logged out + +- ([#32588](https://github.com/RocketChat/Rocket.Chat/pull/32588) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes issues causing nonstop sound notification when taking a chat from the `Current Chats` view + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.9.1 + - @rocket.chat/rest-typings@6.9.1 + - @rocket.chat/api-client@0.1.34 + - @rocket.chat/license@0.1.16 + - @rocket.chat/omnichannel-services@0.1.16 + - @rocket.chat/pdf-worker@0.0.40 + - @rocket.chat/presence@0.1.16 + - @rocket.chat/apps@0.0.7 + - @rocket.chat/core-services@0.3.16 + - @rocket.chat/cron@0.0.36 + - @rocket.chat/fuselage-ui-kit@7.0.1 + - @rocket.chat/gazzodown@7.0.1 + - @rocket.chat/model-typings@0.4.2 + - @rocket.chat/ui-contexts@7.0.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.40 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.1 + - @rocket.chat/ui-client@7.0.1 + - @rocket.chat/ui-video-conf@7.0.1 + - @rocket.chat/web-ui-registration@7.0.1 + - @rocket.chat/instance-status@0.0.40 +
+ +## 6.9.0 + +### Minor Changes + +- ([#31917](https://github.com/RocketChat/Rocket.Chat/pull/31917)) Introduced a tab layout to the users page and implemented a tab called "All" that lists all users. + +- ([#32439](https://github.com/RocketChat/Rocket.Chat/pull/32439)) Allow visitors & integrations to access downloaded files after a room has closed. This was a known limitation in our codebase, where visitors were only able to access uploaded files in a livechat conversation while the conversation was open. + +- ([#32233](https://github.com/RocketChat/Rocket.Chat/pull/32233)) Makes the triggers fired by the condition `after-guest-registration` persist on the livechat client, it will persist through reloads and pagination, only reseting when a conversation is closed (no changes were done on the agent side of the conversation) + +- ([#32193](https://github.com/RocketChat/Rocket.Chat/pull/32193)) Adds CheckOption to departments multi selects improving options visibility state + +- ([#32317](https://github.com/RocketChat/Rocket.Chat/pull/32317)) Replace the read receipt receipt indicator in order to improve the accessibility complience + +- ([#32341](https://github.com/RocketChat/Rocket.Chat/pull/32341)) Changes the scrollbar color in order to improve the contrast and accessibility compliance + +- ([#32298](https://github.com/RocketChat/Rocket.Chat/pull/32298)) Added "Rocket.Chat Cloud Workspace ID" to workspace statistics page + +### Patch Changes + +- ([#32393](https://github.com/RocketChat/Rocket.Chat/pull/32393)) Fixed an issue causing monitors to dissapear from a saved unit every time a user saved the item. This was caused by the UI not sending the correct \_id of the monitors that were already saved, and this caused the Backend to ignore them and remove from the list. + +- ([#31695](https://github.com/RocketChat/Rocket.Chat/pull/31695)) Fix an issue where read receipts menu item wasn't considering the enabled setting to be displayed + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32454](https://github.com/RocketChat/Rocket.Chat/pull/32454)) Fixes an issue not allowing override retention policy in channels + +- ([#32444](https://github.com/RocketChat/Rocket.Chat/pull/32444)) Fixed an issue that prevented CAS users from being merged with existing user data on login + +- ([#32289](https://github.com/RocketChat/Rocket.Chat/pull/32289)) Fixed a problem in how server was processing errors that was sending 2 ephemeral error messages when @all or @here were used while they were disabled via permissions + +- ([#32348](https://github.com/RocketChat/Rocket.Chat/pull/32348)) Fixed an issue where translations would fallback to english some of the times. + +- ([#32182](https://github.com/RocketChat/Rocket.Chat/pull/32182)) Fixed an issue with object storage settings that was not allowing admins to decide if files generated via "Export conversation" feature were being proxied through server or not. + +- ([#32311](https://github.com/RocketChat/Rocket.Chat/pull/32311)) Fixed multiple issues with PDF generation logic when a quoted message was too big to fit in one single page. This was causing an internal infinite loop within the library (as it tried to make it fit, failing and then trying to fit on next page where the same happened thus causing a loop). + The library was not able to break down some nested views and thus was trying to fit the whole quote on one single page. Logic was updated to allow wrapping of the contents when messages are quoted (so they can span multiple lines) and removed a bunch of unnecesary views from the code. +- ([#32364](https://github.com/RocketChat/Rocket.Chat/pull/32364)) Fixed issue with "Export room as file" feature (`rooms.export` endpoint) generating an empty export when given an invalid date + +- ([#32314](https://github.com/RocketChat/Rocket.Chat/pull/32314)) Fixed an issue on Users converter that was not returning the `statusText` property from users even when the typing indicated property existed. + +- ([#32500](https://github.com/RocketChat/Rocket.Chat/pull/32500)) Fix user not being set as online when setting "Use REST instead of websocket for Meteor calls" is disabled + +- ([#32391](https://github.com/RocketChat/Rocket.Chat/pull/32391)) Fixes link image preview not opening in gallery mode + +- ([#32318](https://github.com/RocketChat/Rocket.Chat/pull/32318)) Fixed error handling for files bigger than NATS max allowed payload. This should prevent PDFs from erroring out when generating from rooms that contain heavy images. + +- ([#32479](https://github.com/RocketChat/Rocket.Chat/pull/32479)) Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + + PS: as a workaround reloading after logout or login in also solves the problem. + +- ([#32345](https://github.com/RocketChat/Rocket.Chat/pull/32345)) Replaces the burger menu with an appropriate button fixing the semantics and mismatching color + +- ([#32414](https://github.com/RocketChat/Rocket.Chat/pull/32414)) Fixes the missing spacing on don`t ask again checkbox inside modals + +- ([#32269](https://github.com/RocketChat/Rocket.Chat/pull/32269)) Fixed a bad behavior with the interaction between OTR system messages & trash collection. We use trash collection as a temporary storage that holds recently deleted items from some collections. Messages is one of those. This was causing "User joined OTR" messages to be viewable when querying the trash collection. + Since OTR messages are by definition private, code was updated to bypass trash collection when removing these special messages. + + Note: this only applies to these system messages. OTR user's messages are not stored on the database. + +- ([#32415](https://github.com/RocketChat/Rocket.Chat/pull/32415)) This fuselage`s bump fixes: + + - The message toolbar visibility on hover (Firefox ESR) + - `Bubble` missing font-family + + [more details](https://github.com/RocketChat/fuselage/releases/tag/%40rocket.chat%2Ffuselage%400.53.7) + +- ([#32398](https://github.com/RocketChat/Rocket.Chat/pull/32398)) Fixed issue with external users being able to reset their passwords even when the "Allow Password Change for OAuth Users" setting is disabled + +- ([#32284](https://github.com/RocketChat/Rocket.Chat/pull/32284)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + +- ([#32342](https://github.com/RocketChat/Rocket.Chat/pull/32342)) bump fuselage adding `AttachmentAuthorName` missing color token + +-
Updated dependencies [ff4e396416, bc50dd54a2, ad86761209, f83bd56cc5, 6205ef14f0, 724ba3a729, ee5cdfc367, 70ab2a7b7b]: + + - @rocket.chat/core-typings@6.9.0 + - @rocket.chat/i18n@0.4.0 + - @rocket.chat/core-services@0.3.15 + - @rocket.chat/omnichannel-services@0.1.15 + - @rocket.chat/pdf-worker@0.0.39 + - @rocket.chat/rest-typings@6.9.0 + - @rocket.chat/fuselage-ui-kit@7.0.0 + - @rocket.chat/ui-kit@0.34.0 + - @rocket.chat/api-client@0.1.33 + - @rocket.chat/license@0.1.15 + - @rocket.chat/presence@0.1.15 + - @rocket.chat/apps@0.0.6 + - @rocket.chat/cron@0.0.35 + - @rocket.chat/gazzodown@7.0.0 + - @rocket.chat/model-typings@0.4.1 + - @rocket.chat/ui-contexts@7.0.0 + - @rocket.chat/web-ui-registration@7.0.0 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.39 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.0 + - @rocket.chat/ui-client@7.0.0 + - @rocket.chat/ui-video-conf@7.0.0 + - @rocket.chat/instance-status@0.0.39 +
+ +## 6.9.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32500](https://github.com/RocketChat/Rocket.Chat/pull/32500)) Fix user not being set as online when setting "Use REST instead of websocket for Meteor calls" is disabled + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.9.0-rc.2 + - @rocket.chat/rest-typings@6.9.0-rc.2 + - @rocket.chat/api-client@0.1.33-rc.2 + - @rocket.chat/license@0.1.15-rc.2 + - @rocket.chat/omnichannel-services@0.1.15-rc.2 + - @rocket.chat/pdf-worker@0.0.39-rc.2 + - @rocket.chat/presence@0.1.15-rc.2 + - @rocket.chat/apps@0.0.6-rc.2 + - @rocket.chat/core-services@0.3.15-rc.2 + - @rocket.chat/cron@0.0.35-rc.2 + - @rocket.chat/fuselage-ui-kit@7.0.0-rc.2 + - @rocket.chat/gazzodown@7.0.0-rc.2 + - @rocket.chat/model-typings@0.4.1-rc.2 + - @rocket.chat/ui-contexts@7.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.39-rc.2 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.0-rc.2 + - @rocket.chat/ui-client@7.0.0-rc.2 + - @rocket.chat/ui-video-conf@7.0.0-rc.2 + - @rocket.chat/web-ui-registration@7.0.0-rc.2 + - @rocket.chat/instance-status@0.0.39-rc.2 +
+ +## 6.9.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32479](https://github.com/RocketChat/Rocket.Chat/pull/32479)) Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + + PS: as a workaround reloading after logout or login in also solves the problem. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.9.0-rc.1 + - @rocket.chat/rest-typings@6.9.0-rc.1 + - @rocket.chat/api-client@0.1.33-rc.1 + - @rocket.chat/license@0.1.15-rc.1 + - @rocket.chat/omnichannel-services@0.1.15-rc.1 + - @rocket.chat/pdf-worker@0.0.39-rc.1 + - @rocket.chat/presence@0.1.15-rc.1 + - @rocket.chat/apps@0.0.6-rc.1 + - @rocket.chat/core-services@0.3.15-rc.1 + - @rocket.chat/cron@0.0.35-rc.1 + - @rocket.chat/fuselage-ui-kit@7.0.0-rc.1 + - @rocket.chat/gazzodown@7.0.0-rc.1 + - @rocket.chat/model-typings@0.4.1-rc.1 + - @rocket.chat/ui-contexts@7.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.39-rc.1 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.0-rc.1 + - @rocket.chat/ui-client@7.0.0-rc.1 + - @rocket.chat/ui-video-conf@7.0.0-rc.1 + - @rocket.chat/web-ui-registration@7.0.0-rc.1 + - @rocket.chat/instance-status@0.0.39-rc.1 +
+ +## 6.9.0-rc.0 + +### Minor Changes + +- ([#31917](https://github.com/RocketChat/Rocket.Chat/pull/31917)) Introduced a tab layout to the users page and implemented a tab called "All" that lists all users. + +- ([#32439](https://github.com/RocketChat/Rocket.Chat/pull/32439)) Allow visitors & integrations to access downloaded files after a room has closed. This was a known limitation in our codebase, where visitors were only able to access uploaded files in a livechat conversation while the conversation was open. + +- ([#32233](https://github.com/RocketChat/Rocket.Chat/pull/32233)) Makes the triggers fired by the condition `after-guest-registration` persist on the livechat client, it will persist through reloads and pagination, only reseting when a conversation is closed (no changes were done on the agent side of the conversation) + +- ([#32193](https://github.com/RocketChat/Rocket.Chat/pull/32193)) Adds CheckOption to departments multi selects improving options visibility state + +- ([#32317](https://github.com/RocketChat/Rocket.Chat/pull/32317)) Replace the read receipt receipt indicator in order to improve the accessibility complience + +- ([#32341](https://github.com/RocketChat/Rocket.Chat/pull/32341)) Changes the scrollbar color in order to improve the contrast and accessibility compliance + +- ([#32298](https://github.com/RocketChat/Rocket.Chat/pull/32298)) Added "Rocket.Chat Cloud Workspace ID" to workspace statistics page + +### Patch Changes + +- ([#32393](https://github.com/RocketChat/Rocket.Chat/pull/32393)) Fixed an issue causing monitors to dissapear from a saved unit every time a user saved the item. This was caused by the UI not sending the correct \_id of the monitors that were already saved, and this caused the Backend to ignore them and remove from the list. + +- ([#31695](https://github.com/RocketChat/Rocket.Chat/pull/31695)) Fix an issue where read receipts menu item wasn't considering the enabled setting to be displayed + +- ([#32454](https://github.com/RocketChat/Rocket.Chat/pull/32454)) Fixes an issue not allowing override retention policy in channels + +- ([#32444](https://github.com/RocketChat/Rocket.Chat/pull/32444)) Fixed an issue that prevented CAS users from being merged with existing user data on login + +- ([#32289](https://github.com/RocketChat/Rocket.Chat/pull/32289)) Fixed a problem in how server was processing errors that was sending 2 ephemeral error messages when @all or @here were used while they were disabled via permissions + +- ([#32348](https://github.com/RocketChat/Rocket.Chat/pull/32348)) Fixed an issue where translations would fallback to english some of the times. + +- ([#32182](https://github.com/RocketChat/Rocket.Chat/pull/32182)) Fixed an issue with object storage settings that was not allowing admins to decide if files generated via "Export conversation" feature were being proxied through server or not. + +- ([#32311](https://github.com/RocketChat/Rocket.Chat/pull/32311)) Fixed multiple issues with PDF generation logic when a quoted message was too big to fit in one single page. This was causing an internal infinite loop within the library (as it tried to make it fit, failing and then trying to fit on next page where the same happened thus causing a loop). + The library was not able to break down some nested views and thus was trying to fit the whole quote on one single page. Logic was updated to allow wrapping of the contents when messages are quoted (so they can span multiple lines) and removed a bunch of unnecesary views from the code. +- ([#32364](https://github.com/RocketChat/Rocket.Chat/pull/32364)) Fixed issue with "Export room as file" feature (`rooms.export` endpoint) generating an empty export when given an invalid date + +- ([#32314](https://github.com/RocketChat/Rocket.Chat/pull/32314)) Fixed an issue on Users converter that was not returning the `statusText` property from users even when the typing indicated property existed. + +- ([#32391](https://github.com/RocketChat/Rocket.Chat/pull/32391)) Fixes link image preview not opening in gallery mode + +- ([#32318](https://github.com/RocketChat/Rocket.Chat/pull/32318)) Fixed error handling for files bigger than NATS max allowed payload. This should prevent PDFs from erroring out when generating from rooms that contain heavy images. + +- ([#32345](https://github.com/RocketChat/Rocket.Chat/pull/32345)) Replaces the burger menu with an appropriate button fixing the semantics and mismatching color + +- ([#32414](https://github.com/RocketChat/Rocket.Chat/pull/32414)) Fixes the missing spacing on don`t ask again checkbox inside modals + +- ([#32269](https://github.com/RocketChat/Rocket.Chat/pull/32269)) Fixed a bad behavior with the interaction between OTR system messages & trash collection. We use trash collection as a temporary storage that holds recently deleted items from some collections. Messages is one of those. This was causing "User joined OTR" messages to be viewable when querying the trash collection. + Since OTR messages are by definition private, code was updated to bypass trash collection when removing these special messages. + + Note: this only applies to these system messages. OTR user's messages are not stored on the database. + +- ([#32415](https://github.com/RocketChat/Rocket.Chat/pull/32415)) This fuselage`s bump fixes: + + - The message toolbar visibility on hover (Firefox ESR) + - `Bubble` missing font-family + + [more details](https://github.com/RocketChat/fuselage/releases/tag/%40rocket.chat%2Ffuselage%400.53.7) + +- ([#32398](https://github.com/RocketChat/Rocket.Chat/pull/32398)) Fixed issue with external users being able to reset their passwords even when the "Allow Password Change for OAuth Users" setting is disabled + +- ([#32284](https://github.com/RocketChat/Rocket.Chat/pull/32284)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + +- ([#32342](https://github.com/RocketChat/Rocket.Chat/pull/32342)) bump fuselage adding `AttachmentAuthorName` missing color token + +-
Updated dependencies [ff4e396416, bc50dd54a2, ad86761209, f83bd56cc5, 6205ef14f0, 724ba3a729, ee5cdfc367, 70ab2a7b7b]: + + - @rocket.chat/core-typings@6.9.0-rc.0 + - @rocket.chat/i18n@0.4.0-rc.0 + - @rocket.chat/core-services@0.3.15-rc.0 + - @rocket.chat/omnichannel-services@0.1.15-rc.0 + - @rocket.chat/pdf-worker@0.0.39-rc.0 + - @rocket.chat/rest-typings@6.9.0-rc.0 + - @rocket.chat/fuselage-ui-kit@7.0.0-rc.0 + - @rocket.chat/ui-kit@0.34.0-rc.0 + - @rocket.chat/api-client@0.1.33-rc.0 + - @rocket.chat/license@0.1.15-rc.0 + - @rocket.chat/presence@0.1.15-rc.0 + - @rocket.chat/apps@0.0.6-rc.0 + - @rocket.chat/cron@0.0.35-rc.0 + - @rocket.chat/gazzodown@7.0.0-rc.0 + - @rocket.chat/model-typings@0.4.1-rc.0 + - @rocket.chat/ui-contexts@7.0.0-rc.0 + - @rocket.chat/web-ui-registration@7.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.39-rc.0 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@3.0.0-rc.0 + - @rocket.chat/ui-client@7.0.0-rc.0 + - @rocket.chat/ui-video-conf@7.0.0-rc.0 + - @rocket.chat/instance-status@0.0.39-rc.0 +
+ +## 6.8.0 + +### Minor Changes + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to get a filtered and paginated list of users. + +- ([#32224](https://github.com/RocketChat/Rocket.Chat/pull/32224)) Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings. + +- ([#32115](https://github.com/RocketChat/Rocket.Chat/pull/32115)) Introduces sidebar navigability, allowing users to navigate on sidebar channels through keyboard + +- ([#29461](https://github.com/RocketChat/Rocket.Chat/pull/29461)) Introduces a resizable Contextualbar allowing users to change the width just by dragging it + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Convert mute/unmute meteor methods to endpoints + +- ([#32084](https://github.com/RocketChat/Rocket.Chat/pull/32084)) Added a new setting to automatically disable users from LDAP that can no longer be found by the background sync + +- ([#31965](https://github.com/RocketChat/Rocket.Chat/pull/31965)) Added the ability to serve .well-known paths directly from Rocket.Chat, if using federation, removing the need for special reverse proxy configuration or another component layer for specific types of reverse proxies / loadbalancers. + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to resend the welcome email to a given user + +- ([#32208](https://github.com/RocketChat/Rocket.Chat/pull/32208)) Added a new notification provider in light of the old FCM API deprecation, now you can choose to use the new provider or the old via the `Push_UseLegacy` setting + +- ([#31976](https://github.com/RocketChat/Rocket.Chat/pull/31976)) Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. + This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. +- ([#32173](https://github.com/RocketChat/Rocket.Chat/pull/32173)) Added "Enable Users" option under "Sync User Active State" LDAP setting to allow only re-enabling users found on LDAP background sync + +- ([#31865](https://github.com/RocketChat/Rocket.Chat/pull/31865)) Redesign Save E2EE password modal + +- ([#32272](https://github.com/RocketChat/Rocket.Chat/pull/32272)) Support Message Custom Fields on upload API via field `customField` and JSON value + +- ([#32055](https://github.com/RocketChat/Rocket.Chat/pull/32055)) feat: `ConnectionStatusBar` redesign + +- ([#32073](https://github.com/RocketChat/Rocket.Chat/pull/32073)) Fixed an issue affecting the update modal/contextual bar by apps when it comes to error handling and regular surface update + +### Patch Changes + +- ([#31996](https://github.com/RocketChat/Rocket.Chat/pull/31996)) Fixed Security tab visibility to allow password changes when 2FA/E2E is disabled. + +- ([#32210](https://github.com/RocketChat/Rocket.Chat/pull/32210)) Fixes error `audio.pause() is not a function` and makes the continuous new room notification (livechat) respect the volume set in user preferences. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32186](https://github.com/RocketChat/Rocket.Chat/pull/32186)) Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB + +- ([#32216](https://github.com/RocketChat/Rocket.Chat/pull/32216)) Fixed an issue while creating tokens via the special `users.createToken` API was not respecting the maximum login tokens allowed for a user. + + The following endpoint was deprecated and will be removed on version `8.0.0`: + + - `/api/v1/users.createToken` + + The following Meteor method (realtime API) was deprecated and will be removed on version `8.0.0`: + + - `createToken` + +- ([#31958](https://github.com/RocketChat/Rocket.Chat/pull/31958)) Force logout the clients which are actively online, whenever a user resets E2EE keys. + +- ([#31989](https://github.com/RocketChat/Rocket.Chat/pull/31989)) Fixed issue with login via SAML not redirecting to invite link + +- ([#32187](https://github.com/RocketChat/Rocket.Chat/pull/32187)) Fixes an issue that forces the focus on the last message when interacting by mouse on message list + +- ([#31765](https://github.com/RocketChat/Rocket.Chat/pull/31765)) Fixes the livechat client ignoring the `livechat_fileuploads_enabled` setting when uploading files + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Deprecate muteUserInRoom and unmuteUserInRoom meteor methods + +- ([#32287](https://github.com/RocketChat/Rocket.Chat/pull/32287)) Fixed wrong `Business hours` validations between different weeks + +- ([#32348](https://github.com/RocketChat/Rocket.Chat/pull/32348)) Fixed an issue where translations would fallback to english some of the times. + +- ([#31990](https://github.com/RocketChat/Rocket.Chat/pull/31990)) Fixed open expanded view (galery mode) for image attachments sent by livechat widget + +- ([#32248](https://github.com/RocketChat/Rocket.Chat/pull/32248)) Fixes an issue where the last threads list item wasn't displaying properly + +- ([#32112](https://github.com/RocketChat/Rocket.Chat/pull/32112)) fixed an issue where mentioning a team would trigger the bot message warning that the team is not a part of the channel + +- ([#32069](https://github.com/RocketChat/Rocket.Chat/pull/32069)) Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. + +- ([#32063](https://github.com/RocketChat/Rocket.Chat/pull/32063)) Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. + As UI allowed but changes were not saved, this gave the impression that the function was not working. +- ([#32374](https://github.com/RocketChat/Rocket.Chat/pull/32374)) Fixed an issue with some apps that didn't implement executeViewCloseHandler. This causes opened modals to be open forever on UI (unless Esc was clicked). This is because when the UI attempts to close it, it calls the aforementioned handler, and since it didn't exist, apps engine errored out. + + This returned an empty response to the UI, which ignored the response and continued to show the view. + +- ([#32237](https://github.com/RocketChat/Rocket.Chat/pull/32237)) **Fixed settings-related statistics not being updated according to the license.** + + We've identified an issue where certain statistics were not reflecting recent license changes. This resulted in outdated information being reported for workspaces. + This change ensures that all reported statistics are current and consider the workspace license. + +- ([#32170](https://github.com/RocketChat/Rocket.Chat/pull/32170)) Fixed a language priority issue. It should now respect the following order: User Preference -> Browser Language -> Server Language + +- ([#32202](https://github.com/RocketChat/Rocket.Chat/pull/32202)) Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. + +- ([#32220](https://github.com/RocketChat/Rocket.Chat/pull/32220)) Fixed supported versions not being updated in airgapped environments + +- ([#32141](https://github.com/RocketChat/Rocket.Chat/pull/32141)) Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels. + This change is to keep the semantics and conventions of the endpoints +- ([#32157](https://github.com/RocketChat/Rocket.Chat/pull/32157)) Fixed custom OAuth roles not synced on the first login (on user creation) + +- ([#32136](https://github.com/RocketChat/Rocket.Chat/pull/32136)) Fixes the missing space between name and user name on system messages + +- ([#32007](https://github.com/RocketChat/Rocket.Chat/pull/32007)) This PR have made enhancements to the select and multiselect inputs related to Omnichannel Departments, now the options properly display the complete department names, ensuring clarity for users and added text wrapping for long department names, enhancing readability and UX. + +- ([#32221](https://github.com/RocketChat/Rocket.Chat/pull/32221)) Fixed an issue where an endpoint was called before checking configuration that enables automatic translation when launching the application + +- ([#32230](https://github.com/RocketChat/Rocket.Chat/pull/32230)) Fixed a problem that caused OTR Session messages' to not being transmitted from one peer to another when running Rocket.Chat as microservices. This was caused by a legacy streamer that tried to use the websocket directly, which works on monolith but doesn't on microservices, cause these events are routed through DDP Streamer service. + +- ([#32021](https://github.com/RocketChat/Rocket.Chat/pull/32021)) Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button + +- ([#32123](https://github.com/RocketChat/Rocket.Chat/pull/32123)) fixed search room not showing the new name room name changes + +- ([#30309](https://github.com/RocketChat/Rocket.Chat/pull/30309)) Fixed a problem that caused `afterCreateUser` callback to be called without new user's roles inside. This caused Omnichannel Business Hour manager to ignore these users from assigning open business hours until the manager restarted or the business hour restarted. + +- ([#32172](https://github.com/RocketChat/Rocket.Chat/pull/32172)) Fixes an issue where message reactions are vertically misaligned when zooming out + +- ([#32062](https://github.com/RocketChat/Rocket.Chat/pull/32062)) Fixed an issue where old exports would get overwritten by new ones if generated on the same day, when using external storage services (such as Amazon S3) + +- ([#32284](https://github.com/RocketChat/Rocket.Chat/pull/32284)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + +-
Updated dependencies [845fd64f45, c47a8e3514, 9a6a7d0a40, da45cb6998, 845fd64f45, b94ca7c30b, 9902554388, 8b0986d15a, 4aba7c8a26, c4e58afd8b, c9a92e6ea2, c0d54d742a]: + + - @rocket.chat/rest-typings@6.8.0 + - @rocket.chat/core-typings@6.8.0 + - @rocket.chat/i18n@0.3.0 + - @rocket.chat/model-typings@0.4.0 + - @rocket.chat/apps@0.0.5 + - @rocket.chat/core-services@0.3.14 + - @rocket.chat/fuselage-ui-kit@6.0.0 + - @rocket.chat/presence@0.1.14 + - @rocket.chat/ui-contexts@6.0.0 + - @rocket.chat/api-client@0.1.32 + - @rocket.chat/omnichannel-services@0.1.14 + - @rocket.chat/license@0.1.14 + - @rocket.chat/pdf-worker@0.0.38 + - @rocket.chat/cron@0.0.34 + - @rocket.chat/gazzodown@6.0.0 + - @rocket.chat/web-ui-registration@6.0.0 + - @rocket.chat/models@0.0.38 + - @rocket.chat/base64@1.0.13 + - @rocket.chat/instance-status@0.0.38 + - @rocket.chat/message-parser@0.31.29 + - @rocket.chat/random@1.2.2 + - @rocket.chat/sha256@1.0.10 + - @rocket.chat/ui-composer@0.1.0 + - @rocket.chat/ui-kit@0.33.0 + - @rocket.chat/ui-video-conf@6.0.0 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0 + - @rocket.chat/ui-client@6.0.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ +## 6.8.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32348](https://github.com/RocketChat/Rocket.Chat/pull/32348)) Fixed an issue where translations would fallback to english some of the times. + +- ([#32374](https://github.com/RocketChat/Rocket.Chat/pull/32374)) Fixed an issue with some apps that didn't implement executeViewCloseHandler. This causes opened modals to be open forever on UI (unless Esc was clicked). This is because when the UI attempts to close it, it calls the aforementioned handler, and since it didn't exist, apps engine errored out. + + This returned an empty response to the UI, which ignored the response and continued to show the view. + +-
Updated dependencies [b94ca7c30b]: + + - @rocket.chat/apps@0.0.5-rc.2 + - @rocket.chat/core-services@0.3.14-rc.2 + - @rocket.chat/core-typings@6.8.0-rc.2 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.2 + - @rocket.chat/rest-typings@6.8.0-rc.2 + - @rocket.chat/presence@0.1.14-rc.2 + - @rocket.chat/omnichannel-services@0.1.14-rc.2 + - @rocket.chat/api-client@0.1.32-rc.2 + - @rocket.chat/license@0.1.14-rc.2 + - @rocket.chat/pdf-worker@0.0.38-rc.2 + - @rocket.chat/cron@0.0.34-rc.2 + - @rocket.chat/gazzodown@6.0.0-rc.2 + - @rocket.chat/model-typings@0.4.0-rc.2 + - @rocket.chat/ui-contexts@6.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.38-rc.2 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.2 + - @rocket.chat/ui-client@6.0.0-rc.2 + - @rocket.chat/ui-video-conf@6.0.0-rc.2 + - @rocket.chat/web-ui-registration@6.0.0-rc.2 + - @rocket.chat/instance-status@0.0.38-rc.2 +
+ +## 6.8.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.8.0-rc.1 + - @rocket.chat/rest-typings@6.8.0-rc.1 + - @rocket.chat/api-client@0.1.31-rc.1 + - @rocket.chat/license@0.1.13-rc.1 + - @rocket.chat/omnichannel-services@0.1.13-rc.1 + - @rocket.chat/pdf-worker@0.0.37-rc.1 + - @rocket.chat/presence@0.1.13-rc.1 + - @rocket.chat/apps@0.0.4-rc.1 + - @rocket.chat/core-services@0.3.13-rc.1 + - @rocket.chat/cron@0.0.33-rc.1 + - @rocket.chat/gazzodown@6.0.0-rc.1 + - @rocket.chat/model-typings@0.4.0-rc.1 + - @rocket.chat/ui-contexts@6.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.1 + - @rocket.chat/models@0.0.37-rc.1 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.1 + - @rocket.chat/ui-client@6.0.0-rc.1 + - @rocket.chat/ui-video-conf@6.0.0-rc.1 + - @rocket.chat/web-ui-registration@6.0.0-rc.1 + - @rocket.chat/instance-status@0.0.37-rc.1 +
+ +## 6.8.0-rc.0 + +### Minor Changes + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to get a filtered and paginated list of users. + +- ([#32224](https://github.com/RocketChat/Rocket.Chat/pull/32224)) Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings. + +- ([#32115](https://github.com/RocketChat/Rocket.Chat/pull/32115)) Introduces sidebar navigability, allowing users to navigate on sidebar channels through keyboard + +- ([#29461](https://github.com/RocketChat/Rocket.Chat/pull/29461)) Introduces a resizable Contextualbar allowing users to change the width just by dragging it + +- ([#31840](https://github.com/RocketChat/Rocket.Chat/pull/31840)) Encrypt file descriptions in E2EE rooms + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Convert mute/unmute meteor methods to endpoints + +- ([#32084](https://github.com/RocketChat/Rocket.Chat/pull/32084)) Added a new setting to automatically disable users from LDAP that can no longer be found by the background sync + +- ([#31965](https://github.com/RocketChat/Rocket.Chat/pull/31965)) Added the ability to serve .well-known paths directly from Rocket.Chat, if using federation, removing the need for special reverse proxy configuration or another component layer for specific types of reverse proxies / loadbalancers. + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to resend the welcome email to a given user + +- ([#32208](https://github.com/RocketChat/Rocket.Chat/pull/32208)) Added a new notification provider in light of the old FCM API deprecation, now you can choose to use the new provider or the old via the `Push_UseLegacy` setting + +- ([#31976](https://github.com/RocketChat/Rocket.Chat/pull/31976)) Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. + This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. +- ([#32173](https://github.com/RocketChat/Rocket.Chat/pull/32173)) Added "Enable Users" option under "Sync User Active State" LDAP setting to allow only re-enabling users found on LDAP background sync + +- ([#31865](https://github.com/RocketChat/Rocket.Chat/pull/31865)) Redesign Save E2EE password modal + +- ([#32272](https://github.com/RocketChat/Rocket.Chat/pull/32272)) Support Message Custom Fields on upload API via field `customField` and JSON value + +- ([#32055](https://github.com/RocketChat/Rocket.Chat/pull/32055)) feat: `ConnectionStatusBar` redesign + +- ([#32073](https://github.com/RocketChat/Rocket.Chat/pull/32073)) Fixed an issue affecting the update modal/contextual bar by apps when it comes to error handling and regular surface update + +### Patch Changes + +- ([#31996](https://github.com/RocketChat/Rocket.Chat/pull/31996)) Fixed Security tab visibility to allow password changes when 2FA/E2E is disabled. + +- ([#32210](https://github.com/RocketChat/Rocket.Chat/pull/32210)) Fixes error `audio.pause() is not a function` and makes the continuous new room notification (livechat) respect the volume set in user preferences. + +- ([#32186](https://github.com/RocketChat/Rocket.Chat/pull/32186)) Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB + +- ([#32216](https://github.com/RocketChat/Rocket.Chat/pull/32216)) Fixed an issue while creating tokens via the special `users.createToken` API was not respecting the maximum login tokens allowed for a user. + + The following endpoint was deprecated and will be removed on version `8.0.0`: + + - `/api/v1/users.createToken` + + The following Meteor method (realtime API) was deprecated and will be removed on version `8.0.0`: + + - `createToken` + +- ([#31958](https://github.com/RocketChat/Rocket.Chat/pull/31958)) Force logout the clients which are actively online, whenever a user resets E2EE keys. + +- ([#31989](https://github.com/RocketChat/Rocket.Chat/pull/31989)) Fixed issue with login via SAML not redirecting to invite link + +- ([#32187](https://github.com/RocketChat/Rocket.Chat/pull/32187)) Fixes an issue that forces the focus on the last message when interacting by mouse on message list + +- ([#31765](https://github.com/RocketChat/Rocket.Chat/pull/31765)) Fixes the livechat client ignoring the `livechat_fileuploads_enabled` setting when uploading files + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Deprecate muteUserInRoom and unmuteUserInRoom meteor methods + +- ([#32287](https://github.com/RocketChat/Rocket.Chat/pull/32287)) Fixed wrong `Business hours` validations between different weeks + +- ([#31990](https://github.com/RocketChat/Rocket.Chat/pull/31990)) Fixed open expanded view (galery mode) for image attachments sent by livechat widget + +- ([#32248](https://github.com/RocketChat/Rocket.Chat/pull/32248)) Fixes an issue where the last threads list item wasn't displaying properly + +- ([#32112](https://github.com/RocketChat/Rocket.Chat/pull/32112)) fixed an issue where mentioning a team would trigger the bot message warning that the team is not a part of the channel + +- ([#32069](https://github.com/RocketChat/Rocket.Chat/pull/32069)) Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. + +- ([#32063](https://github.com/RocketChat/Rocket.Chat/pull/32063)) Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. + As UI allowed but changes were not saved, this gave the impression that the function was not working. +- ([#32237](https://github.com/RocketChat/Rocket.Chat/pull/32237)) **Fixed settings-related statistics not being updated according to the license.** + + We've identified an issue where certain statistics were not reflecting recent license changes. This resulted in outdated information being reported for workspaces. + This change ensures that all reported statistics are current and consider the workspace license. + +- ([#32170](https://github.com/RocketChat/Rocket.Chat/pull/32170)) Fixed a language priority issue. It should now respect the following order: User Preference -> Browser Language -> Server Language + +- ([#32202](https://github.com/RocketChat/Rocket.Chat/pull/32202)) Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. + +- ([#32220](https://github.com/RocketChat/Rocket.Chat/pull/32220)) Fixed supported versions not being updated in airgapped environments + +- ([#32141](https://github.com/RocketChat/Rocket.Chat/pull/32141)) Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels. + This change is to keep the semantics and conventions of the endpoints +- ([#32157](https://github.com/RocketChat/Rocket.Chat/pull/32157)) Fixed custom OAuth roles not synced on the first login (on user creation) + +- ([#32136](https://github.com/RocketChat/Rocket.Chat/pull/32136)) Fixes the missing space between name and user name on system messages + +- ([#32007](https://github.com/RocketChat/Rocket.Chat/pull/32007)) This PR have made enhancements to the select and multiselect inputs related to Omnichannel Departments, now the options properly display the complete department names, ensuring clarity for users and added text wrapping for long department names, enhancing readability and UX. + +- ([#32221](https://github.com/RocketChat/Rocket.Chat/pull/32221)) Fixed an issue where an endpoint was called before checking configuration that enables automatic translation when launching the application + +- ([#32230](https://github.com/RocketChat/Rocket.Chat/pull/32230)) Fixed a problem that caused OTR Session messages' to not being transmitted from one peer to another when running Rocket.Chat as microservices. This was caused by a legacy streamer that tried to use the websocket directly, which works on monolith but doesn't on microservices, cause these events are routed through DDP Streamer service. + +- ([#32021](https://github.com/RocketChat/Rocket.Chat/pull/32021)) Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button + +- ([#32123](https://github.com/RocketChat/Rocket.Chat/pull/32123)) fixed search room not showing the new name room name changes + +- ([#30309](https://github.com/RocketChat/Rocket.Chat/pull/30309)) Fixed a problem that caused `afterCreateUser` callback to be called without new user's roles inside. This caused Omnichannel Business Hour manager to ignore these users from assigning open business hours until the manager restarted or the business hour restarted. + +- ([#32172](https://github.com/RocketChat/Rocket.Chat/pull/32172)) Fixes an issue where message reactions are vertically misaligned when zooming out + +- ([#32062](https://github.com/RocketChat/Rocket.Chat/pull/32062)) Fixed an issue where old exports would get overwritten by new ones if generated on the same day, when using external storage services (such as Amazon S3) + +-
Updated dependencies [845fd64f45, c47a8e3514, 9a6a7d0a40, da45cb6998, 845fd64f45, 9902554388, 8b0986d15a, 4aba7c8a26, c4e58afd8b, c9a92e6ea2, c0d54d742a]: + + - @rocket.chat/rest-typings@6.8.0-rc.0 + - @rocket.chat/core-typings@6.8.0-rc.0 + - @rocket.chat/i18n@0.3.0-rc.0 + - @rocket.chat/model-typings@0.4.0-rc.0 + - @rocket.chat/core-services@0.3.12-rc.0 + - @rocket.chat/ui-contexts@6.0.0-rc.0 + - @rocket.chat/api-client@0.1.30-rc.0 + - @rocket.chat/omnichannel-services@0.1.12-rc.0 + - @rocket.chat/presence@0.1.12-rc.0 + - @rocket.chat/license@0.1.12-rc.0 + - @rocket.chat/pdf-worker@0.0.36-rc.0 + - @rocket.chat/apps@0.0.3-rc.0 + - @rocket.chat/cron@0.0.32-rc.0 + - @rocket.chat/gazzodown@6.0.0-rc.0 + - @rocket.chat/web-ui-registration@6.0.0-rc.0 + - @rocket.chat/models@0.0.36-rc.0 + - @rocket.chat/base64@1.0.13 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.0 + - @rocket.chat/instance-status@0.0.36-rc.0 + - @rocket.chat/message-parser@0.31.29 + - @rocket.chat/random@1.2.2 + - @rocket.chat/sha256@1.0.10 + - @rocket.chat/ui-composer@0.1.0 + - @rocket.chat/ui-kit@0.33.0 + - @rocket.chat/ui-video-conf@6.0.0-rc.0 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.0 + - @rocket.chat/ui-client@6.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 + +- Bump @rocket.chat/meteor version. + +- ([#32315](https://github.com/RocketChat/Rocket.Chat/pull/32315) by [@dionisio-bot](https://github.com/dionisio-bot)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.7.2 + - @rocket.chat/rest-typings@6.7.2 + - @rocket.chat/api-client@0.1.31 + - @rocket.chat/license@0.1.13 + - @rocket.chat/omnichannel-services@0.1.13 + - @rocket.chat/pdf-worker@0.0.37 + - @rocket.chat/presence@0.1.13 + - @rocket.chat/apps@0.0.4 + - @rocket.chat/core-services@0.3.13 + - @rocket.chat/cron@0.0.33 + - @rocket.chat/gazzodown@5.0.2 + - @rocket.chat/model-typings@0.3.9 + - @rocket.chat/ui-contexts@5.0.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@5.0.2 + - @rocket.chat/models@0.0.37 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@1.0.2 + - @rocket.chat/ui-client@5.0.2 + - @rocket.chat/ui-video-conf@5.0.2 + - @rocket.chat/web-ui-registration@5.0.2 + - @rocket.chat/instance-status@0.0.37 +
+ +## 6.7.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32284](https://github.com/RocketChat/Rocket.Chat/pull/32284)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + +## 6.7.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32253](https://github.com/RocketChat/Rocket.Chat/pull/32253)) Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB + +- ([#32252](https://github.com/RocketChat/Rocket.Chat/pull/32252)) Fixes an issue that forces the focus on the last message when interacting by mouse on message list + +- ([#32256](https://github.com/RocketChat/Rocket.Chat/pull/32256)) Fixed open expanded view (galery mode) for image attachments sent by livechat widget + +- ([#32254](https://github.com/RocketChat/Rocket.Chat/pull/32254)) Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. + +- ([#32265](https://github.com/RocketChat/Rocket.Chat/pull/32265)) Fixed supported versions not being updated in airgapped environments + +- ([#32251](https://github.com/RocketChat/Rocket.Chat/pull/32251)) Fixes an issue where message reactions are vertically misaligned when zooming out + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.7.1 + - @rocket.chat/rest-typings@6.7.1 + - @rocket.chat/api-client@0.1.30 + - @rocket.chat/license@0.1.12 + - @rocket.chat/omnichannel-services@0.1.12 + - @rocket.chat/pdf-worker@0.0.36 + - @rocket.chat/presence@0.1.12 + - @rocket.chat/apps@0.0.3 + - @rocket.chat/core-services@0.3.12 + - @rocket.chat/cron@0.0.32 + - @rocket.chat/gazzodown@5.0.1 + - @rocket.chat/model-typings@0.3.8 + - @rocket.chat/ui-contexts@5.0.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@5.0.1 + - @rocket.chat/models@0.0.36 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@1.0.1 + - @rocket.chat/ui-client@5.0.1 + - @rocket.chat/ui-video-conf@5.0.1 + - @rocket.chat/web-ui-registration@5.0.1 + - @rocket.chat/instance-status@0.0.36 +
+ ## 6.7.0 ### Minor Changes diff --git a/apps/meteor/app/2fa/server/functions/resetTOTP.ts b/apps/meteor/app/2fa/server/functions/resetTOTP.ts index 85fe696babe10..3be8ec7c80602 100644 --- a/apps/meteor/app/2fa/server/functions/resetTOTP.ts +++ b/apps/meteor/app/2fa/server/functions/resetTOTP.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; import { isUserIdFederated } from '../../../../server/lib/isUserIdFederated'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -68,6 +69,14 @@ export async function resetTOTP(userId: string, notifyUser = false): Promise({ // Once the TOTP is validated we logout all other clients const { 'x-auth-token': xAuthToken } = this.connection?.httpHeaders ?? {}; - if (xAuthToken) { + if (xAuthToken && this.userId) { const hashedToken = Accounts._hashLoginToken(xAuthToken); - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new Meteor.Error('error-logging-out-other-clients', 'Error logging out other clients'); + const { modifiedCount } = await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken); + + if (modifiedCount > 0) { + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + if (!this.userId) { + return; + } + const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + return { + clientAction: 'updated', + id: this.userId, + diff: { 'services.resume.loginTokens': userTokens?.services?.resume?.loginTokens }, + }; + }); } } diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 87153440cd28b..5a87843f47953 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -14,9 +14,11 @@ import { Restivus } from 'meteor/rocketchat:restivus'; import _ from 'underscore'; import { isObject } from '../../../lib/utils/isObject'; +import { getNestedProp } from '../../../server/lib/getNestedProp'; import { getRestPayload } from '../../../server/lib/logger/logPayloads'; import { checkCodeForUser } from '../../2fa/server/code'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; import { metrics } from '../../metrics/server'; import { settings } from '../../settings/server'; import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; @@ -848,6 +850,19 @@ export class APIClass extends Restivus { }, ); + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(this.user._id, { projection: { [tokenPath]: 1 } }); + if (!userTokens) { + return; + } + + const diff = { [tokenPath]: getNestedProp(userTokens, tokenPath) }; + + return { clientAction: 'updated', id: this.user._id, diff }; + }); + const response = { status: 'success', data: { diff --git a/apps/meteor/app/api/server/lib/emailInbox.ts b/apps/meteor/app/api/server/lib/emailInbox.ts index 663459c7bc052..304d297261af7 100644 --- a/apps/meteor/app/api/server/lib/emailInbox.ts +++ b/apps/meteor/app/api/server/lib/emailInbox.ts @@ -1,6 +1,8 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; import { EmailInbox, Users } from '@rocket.chat/models'; -import type { Filter, InsertOneResult, Sort, UpdateResult, WithId } from 'mongodb'; +import type { DeleteResult, Filter, InsertOneResult, Sort } from 'mongodb'; + +import { notifyOnEmailInboxChanged } from '../../../lib/server/lib/notifyListener'; export const findEmailInboxes = async ({ query = {}, @@ -34,33 +36,31 @@ export const findEmailInboxes = async ({ }; }; -export const findOneEmailInbox = async ({ _id }: { _id: string }): Promise => { - return EmailInbox.findOneById(_id); -}; export const insertOneEmailInbox = async ( userId: string, emailInboxParams: Pick, -): Promise>> => { +): Promise> => { const obj = { ...emailInboxParams, _createdAt: new Date(), _updatedAt: new Date(), _createdBy: await Users.findOneById(userId, { projection: { username: 1 } }), }; - return EmailInbox.insertOne(obj); + + const response = await EmailInbox.create(obj); + + if (response.insertedId) { + void notifyOnEmailInboxChanged({ _id: response.insertedId, ...obj }, 'inserted'); + } + + return response; }; export const updateEmailInbox = async ( emailInboxParams: Pick, -): Promise> | UpdateResult> => { +): Promise | null> => { const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; - const emailInbox = await findOneEmailInbox({ _id }); - - if (!emailInbox) { - throw new Error('error-invalid-email-inbox'); - } - const updateEmailInbox = { $set: { active, @@ -76,5 +76,29 @@ export const updateEmailInbox = async ( ...(department === 'All' && { $unset: { department: 1 as const } }), }; - return EmailInbox.updateOne({ _id }, updateEmailInbox); + const updatedResponse = await EmailInbox.updateById(_id, updateEmailInbox); + + if (!updatedResponse.value) { + throw new Error('error-invalid-email-inbox'); + } + + void notifyOnEmailInboxChanged( + { + ...updatedResponse.value, + ...(department === 'All' && { department: undefined }), + }, + 'updated', + ); + + return updatedResponse.value; +}; + +export const removeEmailInbox = async (emailInboxId: IEmailInbox['_id']): Promise => { + const removeResponse = await EmailInbox.removeById(emailInboxId); + + if (removeResponse.deletedCount) { + void notifyOnEmailInboxChanged({ _id: emailInboxId }, 'removed'); + } + + return removeResponse; }; diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts index 9a0e7e4e11c9c..020988b0aca42 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.ts @@ -5,6 +5,7 @@ import { getCachedSupportedVersionsToken, wrapPromise, } from '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken'; +import { settings } from '../../../settings/server'; import { Info, minimumClientVersions } from '../../../utils/rocketchat.info'; const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1'); @@ -12,10 +13,10 @@ const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+) export async function getServerInfo(userId?: string): Promise { const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics')); const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken()); + const cloudWorkspaceId = settings.get('Cloud_Workspace_Id'); return { version: removePatchInfo(Info.version), - ...(hasPermissionToViewStatistics && { info: { ...Info, @@ -28,5 +29,7 @@ export async function getServerInfo(userId?: string): Promise { supportedVersionsToken.result && { supportedVersions: { signed: supportedVersionsToken.result }, }), + + cloudWorkspaceId, }; } diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 9b8f69fb3a66d..85fc0658542d4 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -5,6 +5,8 @@ import type { ValidateFunction } from 'ajv'; import busboy from 'busboy'; import type { Request } from 'express'; +import { getMimeType } from '../../../utils/lib/mimeTypes'; + type UploadResult = { file: Readable & { truncated: boolean }; fieldname: string; @@ -61,7 +63,7 @@ export async function getUploadFormData< function onFile( fieldname: string, file: Readable & { truncated: boolean }, - { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, + { filename, encoding }: { filename: string; encoding: string }, ) { if (options.field && fieldname !== options.field) { file.resume(); @@ -83,7 +85,7 @@ export async function getUploadFormData< file, filename, encoding, - mimetype, + mimetype: getMimeType(filename), fieldname, fields, fileBuffer: Buffer.concat(fileChunks), diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index 14d3e20502fda..3f1353be8a6c2 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -98,6 +98,7 @@ export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: strin name: 1, t: 1, avatarETag: 1, + encrypted: 1, }, limit: 10, sort: { diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index f80d662771dfd..a912043fc656d 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -154,6 +154,7 @@ export async function findPaginatedUsersByStatus({ lastLogin: 1, type: 1, reason: 1, + federated: 1, }; const actualSort: Record = sort || { username: 1 }; diff --git a/apps/meteor/app/api/server/v1/banners.ts b/apps/meteor/app/api/server/v1/banners.ts index 4dc74208153b3..48c94d3711bdd 100644 --- a/apps/meteor/app/api/server/v1/banners.ts +++ b/apps/meteor/app/api/server/v1/banners.ts @@ -52,9 +52,8 @@ import { API } from '../api'; */ API.v1.addRoute( 'banners.getNew', - { authRequired: true }, + { authRequired: true, deprecation: { version: '8.0.0', alternatives: ['banners/:id', 'banners'] } }, { - // deprecated async get() { check( this.queryParams, diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 8cb3e8ab4236e..74bd85dded6a0 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -1,13 +1,18 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; import { ise2eGetUsersOfRoomWithoutKeyParamsGET, ise2eSetRoomKeyIDParamsPOST, ise2eSetUserPublicAndPrivateKeysParamsPOST, ise2eUpdateGroupKeyParamsPOST, + isE2EProvideUsersGroupKeyProps, + isE2EFetchUsersWaitingForGroupKeyProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey'; +import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys'; +import { settings } from '../../../settings/server'; import { API } from '../api'; API.v1.addRoute( @@ -113,6 +118,8 @@ API.v1.addRoute( * type: string * private_key: * type: string + * force: + * type: boolean * responses: * 200: * content: @@ -135,11 +142,12 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { public_key, private_key } = this.bodyParams; + const { public_key, private_key, force } = this.bodyParams; await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { public_key, private_key, + force, }); return API.v1.success(); @@ -185,6 +193,9 @@ API.v1.addRoute( { authRequired: true, validateParams: ise2eUpdateGroupKeyParamsPOST, + deprecation: { + version: '8.0.0', + }, }, { async post() { @@ -230,3 +241,46 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'e2e.fetchUsersWaitingForGroupKey', + { + authRequired: true, + validateParams: isE2EFetchUsersWaitingForGroupKeyProps, + }, + { + async get() { + if (!settings.get('E2E_Enable')) { + return API.v1.success({ usersWaitingForE2EKeys: {} }); + } + + const { roomIds = [] } = this.queryParams; + const usersWaitingForE2EKeys = (await Subscriptions.findUsersWithPublicE2EKeyByRids(roomIds, this.userId).toArray()).reduce< + Record + >((acc, { rid, users }) => ({ [rid]: users, ...acc }), {}); + + return API.v1.success({ + usersWaitingForE2EKeys, + }); + }, + }, +); + +API.v1.addRoute( + 'e2e.provideUsersSuggestedGroupKeys', + { + authRequired: true, + validateParams: isE2EProvideUsersGroupKeyProps, + }, + { + async post() { + if (!settings.get('E2E_Enable')) { + return API.v1.success(); + } + + await provideUsersSuggestedGroupKeys(this.userId, this.bodyParams.usersSuggestedGroupKeys); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/email-inbox.ts b/apps/meteor/app/api/server/v1/email-inbox.ts index 5748565a0f773..89ede496b78ac 100644 --- a/apps/meteor/app/api/server/v1/email-inbox.ts +++ b/apps/meteor/app/api/server/v1/email-inbox.ts @@ -4,7 +4,7 @@ import { check, Match } from 'meteor/check'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -import { insertOneEmailInbox, findEmailInboxes, findOneEmailInbox, updateEmailInbox } from '../lib/emailInbox'; +import { insertOneEmailInbox, findEmailInboxes, updateEmailInbox, removeEmailInbox } from '../lib/emailInbox'; API.v1.addRoute( 'email-inbox.list', @@ -55,12 +55,23 @@ API.v1.addRoute( let _id: string; if (!emailInboxParams?._id) { - const emailInbox = await insertOneEmailInbox(this.userId, emailInboxParams); - _id = emailInbox.insertedId.toString(); + const { insertedId } = await insertOneEmailInbox(this.userId, emailInboxParams); + + if (!insertedId) { + return API.v1.failure('Failed to create email inbox'); + } + + _id = insertedId; } else { - _id = emailInboxParams._id; - await updateEmailInbox({ ...emailInboxParams, _id }); + const emailInbox = await updateEmailInbox({ ...emailInboxParams, _id: emailInboxParams._id }); + + if (!emailInbox?._id) { + return API.v1.failure('Failed to update email inbox'); + } + + _id = emailInbox._id; } + return API.v1.success({ _id }); }, }, @@ -79,7 +90,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); @@ -97,11 +108,12 @@ API.v1.addRoute( throw new Error('error-invalid-param'); } - const emailInboxes = await EmailInbox.findOneById(_id); - if (!emailInboxes) { + const { deletedCount } = await removeEmailInbox(_id); + + if (!deletedCount) { return API.v1.notFound(); } - await EmailInbox.removeById(_id); + return API.v1.success({ _id }); }, }, @@ -120,7 +132,7 @@ API.v1.addRoute( // TODO: Chapter day backend - check if user has permission to view this email inbox instead of null values // TODO: Chapter day: Remove this endpoint and move search to GET /email-inbox - const emailInbox = await EmailInbox.findOne({ email }); + const emailInbox = await EmailInbox.findByEmail(email); return API.v1.success({ emailInbox }); }, @@ -140,7 +152,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index bdf6fa2dd1c61..dd4da47bff052 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -24,6 +24,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { getLogs } from '../../../../server/stream/stdout'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { passwordPolicy } from '../../../lib/server'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; @@ -687,27 +688,49 @@ API.v1.addRoute( setDeploymentAs: String, }); + const settingsIds: string[] = []; + if (this.bodyParams.setDeploymentAs === 'new-workspace') { - await Promise.all([ - Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()), - // Settings.resetValueById('Cloud_Url'), - Settings.resetValueById('Cloud_Service_Agree_PrivacyTerms'), - Settings.resetValueById('Cloud_Workspace_Id'), - Settings.resetValueById('Cloud_Workspace_Name'), - Settings.resetValueById('Cloud_Workspace_Client_Id'), - Settings.resetValueById('Cloud_Workspace_Client_Secret'), - Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At'), - Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri'), - Settings.resetValueById('Cloud_Workspace_PublicKey'), - Settings.resetValueById('Cloud_Workspace_License'), - Settings.resetValueById('Cloud_Workspace_Had_Trial'), - Settings.resetValueById('Cloud_Workspace_Access_Token'), - Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)), - Settings.resetValueById('Cloud_Workspace_Registration_State'), - ]); + settingsIds.push( + 'Cloud_Service_Agree_PrivacyTerms', + 'Cloud_Workspace_Id', + 'Cloud_Workspace_Name', + 'Cloud_Workspace_Client_Id', + 'Cloud_Workspace_Client_Secret', + 'Cloud_Workspace_Client_Secret_Expires_At', + 'Cloud_Workspace_Registration_Client_Uri', + 'Cloud_Workspace_PublicKey', + 'Cloud_Workspace_License', + 'Cloud_Workspace_Had_Trial', + 'Cloud_Workspace_Access_Token', + 'uniqueID', + 'Cloud_Workspace_Access_Token_Expires_At', + ); } - await Settings.updateValueById('Deployment_FingerPrint_Verified', true); + settingsIds.push('Deployment_FingerPrint_Verified'); + + const promises = settingsIds.map((settingId) => { + if (settingId === 'uniqueID') { + return Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()); + } + + if (settingId === 'Cloud_Workspace_Access_Token_Expires_At') { + return Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)); + } + + if (settingId === 'Deployment_FingerPrint_Verified') { + return Settings.updateValueById('Deployment_FingerPrint_Verified', true); + } + + return Settings.resetValueById(settingId); + }); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]); + } + }); return API.v1.success({}); }, diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 4b860d6e1eacb..3613cc1713544 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -4,6 +4,7 @@ import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { API } from '../api'; API.v1.addRoute( @@ -70,6 +71,7 @@ API.v1.addRoute( for await (const permission of bodyParams.permissions) { await Permissions.setRoles(permission._id, permission.roles); + void notifyOnPermissionChangedById(permission._id); } const result = (await Meteor.callAsync('permissions/get')) as IPermission[]; diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index a0fff9683b80d..66c6677a9eedf 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -9,6 +9,7 @@ import { getUsersInRolePaginated } from '../../../authorization/server/functions import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync, hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnRoleChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server/index'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -179,6 +180,8 @@ API.v1.addRoute( await Roles.removeById(role._id); + void notifyOnRoleChanged(role, 'removed'); + return API.v1.success(); }, }, diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 9576a79f6678e..e3296b98ef178 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -2,10 +2,11 @@ import { Media } from '@rocket.chat/core-services'; import type { IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users, Uploads } from '@rocket.chat/models'; import type { Notifications } from '@rocket.chat/rest-typings'; -import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps } from '@rocket.chat/rest-typings'; +import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps, isRoomsExportProps } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; +import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/methods/eraseRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; @@ -141,7 +142,13 @@ API.v1.addRoute( API.v1.addRoute( 'rooms.upload/:rid', - { authRequired: true }, + { + authRequired: true, + deprecation: { + version: '8.0.0', + alternatives: ['rooms.media'], + }, + }, { async post() { if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { @@ -194,6 +201,112 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.media/:rid', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await getUploadFormData( + { + request: this.request, + }, + { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, + ); + + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + + const { fields } = file; + + let content; + + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); + } + } + + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; + + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } + + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + + return API.v1.success({ + file: { + _id: uploadedFile._id, + url: uploadedFile.path, + }, + }); + }, + }, +); + +API.v1.addRoute( + 'rooms.mediaConfirm/:rid/:fileId', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { + throw new Meteor.Error('invalid-file'); + } + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + + await sendFileMessage( + this.userId, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { parseAttachmentsForE2EE: false }, + ); + + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + + return API.v1.success({ + message, + }); + }, + }, +); + API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, @@ -599,15 +712,11 @@ API.v1.addRoute( API.v1.addRoute( 'rooms.export', - { authRequired: true }, + { authRequired: true, validateParams: isRoomsExportProps }, { async post() { const { rid, type } = this.bodyParams; - if (!rid || !type || !['email', 'file'].includes(type)) { - throw new Meteor.Error('error-invalid-params'); - } - if (!(await hasPermissionAsync(this.userId, 'mail-messages', rid))) { throw new Meteor.Error('error-action-not-allowed', 'Mailing is not allowed'); } @@ -627,12 +736,8 @@ API.v1.addRoute( const { dateFrom, dateTo } = this.bodyParams; const { format } = this.bodyParams; - if (!['html', 'json'].includes(format || '')) { - throw new Meteor.Error('error-invalid-format'); - } - - const convertedDateFrom = new Date(dateFrom || ''); - const convertedDateTo = new Date(dateTo || ''); + const convertedDateFrom = dateFrom ? new Date(dateFrom) : new Date(0); + const convertedDateTo = dateTo ? new Date(dateTo) : new Date(); convertedDateTo.setDate(convertedDateTo.getDate() + 1); if (convertedDateFrom > convertedDateTo) { @@ -658,10 +763,6 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-recipient'); } - if (messages?.length === 0) { - throw new Meteor.Error('error-invalid-messages'); - } - const result = await dataExport.sendViaEmail( { rid, diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index bccfc8d91fc7b..574f4ee64194f 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -13,6 +13,7 @@ import type { FindOptions } from 'mongodb'; import _ from 'underscore'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; import { API } from '../api'; @@ -186,23 +187,34 @@ API.v1.addRoute( } if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { - await Settings.updateOptionsById(this.urlParams._id, { - editor: this.bodyParams.editor, - }); - await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + const updateOptionsPromise = Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); + const updateValuePromise = Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + + const [updateOptionsResult, updateValueResult] = await Promise.all([updateOptionsPromise, updateValuePromise]); + + if (updateOptionsResult.modifiedCount || updateValueResult.modifiedCount) { + await notifyOnSettingChangedById(this.urlParams._id); + } + return API.v1.success(); } - if ( - isSettingsUpdatePropDefault(this.bodyParams) && - (await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) - ) { + if (isSettingsUpdatePropDefault(this.bodyParams)) { + const { matchedCount } = await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + if (!matchedCount) { + return API.v1.failure(); + } + const s = await Settings.findOneNotHiddenById(this.urlParams._id); if (!s) { return API.v1.failure(); } + settings.set(s); setValue(this.urlParams._id, this.bodyParams.value); + + await notifyOnSettingChanged(s); + return API.v1.success(); } diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index ccca23f8ea823..c26957fa19910 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -43,6 +43,7 @@ import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; +import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { generateAccessToken } from '../../../lib/server/methods/createToken'; import { settings } from '../../../settings/server'; import { getURL } from '../../../utils/server/getURL'; @@ -387,7 +388,8 @@ API.v1.addRoute( const lastLoggedIn = new Date(); lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); - const count = (await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false)).modifiedCount; + // since we're deactiving users that are not logged in, there is no need to send data through WS + const { modifiedCount: count } = await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false); return API.v1.success({ count, @@ -861,14 +863,31 @@ API.v1.addRoute( // When 2FA is enable we logout all other clients const xAuthToken = this.request.headers['x-auth-token'] as string; - if (xAuthToken) { - const hashedToken = Accounts._hashLoginToken(xAuthToken); + if (!xAuthToken) { + return API.v1.success(); + } - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); - } + const hashedToken = Accounts._hashLoginToken(xAuthToken); + + if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); } + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + if (!userTokens) { + return; + } + + return { + clientAction: 'updated', + id: this.user._id, + diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, + }; + }); + return API.v1.success(); }, }, @@ -1021,6 +1040,12 @@ API.v1.addRoute( const me = (await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; + void notifyOnUserChange({ + clientAction: 'updated', + id: this.userId, + diff: { 'services.resume.loginTokens': me.services?.resume?.loginTokens }, + }); + const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); const tokenExpires = @@ -1172,6 +1197,8 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } + void notifyOnUserChange({ clientAction: 'updated', id: this.userId, diff: { 'services.resume.loginTokens': [] } }); + return API.v1.success({ message: `User ${userId} has been logged out!`, }); @@ -1242,6 +1269,8 @@ API.v1.addRoute( return API.v1.unauthorized(); } + // TODO refactor to not update the user twice (one inside of `setStatusText` and then later just the status + statusDefault) + if (this.bodyParams.message || this.bodyParams.message === '') { await setStatusText(user._id, this.bodyParams.message); } diff --git a/apps/meteor/app/api/server/v1/voip/extensions.ts b/apps/meteor/app/api/server/v1/voip/extensions.ts index e1c2629feb86a..1a0fecf7cfc5f 100644 --- a/apps/meteor/app/api/server/v1/voip/extensions.ts +++ b/apps/meteor/app/api/server/v1/voip/extensions.ts @@ -1,4 +1,4 @@ -import { Voip } from '@rocket.chat/core-services'; +import { VoipAsterisk } from '@rocket.chat/core-services'; import type { IVoipExtensionBase } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -14,7 +14,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, { async get() { - const version = await Voip.getConnectorVersion(); + const version = await VoipAsterisk.getConnectorVersion(); return API.v1.success(version); }, }, @@ -26,7 +26,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, { async get() { - const list = await Voip.getExtensionList(); + const list = await VoipAsterisk.getExtensionList(); const result = list.result as IVoipExtensionBase[]; return API.v1.success({ extensions: result }); }, @@ -48,7 +48,7 @@ API.v1.addRoute( extension: String, }), ); - const endpointDetails = await Voip.getExtensionDetails(this.queryParams); + const endpointDetails = await VoipAsterisk.getExtensionDetails(this.queryParams); return API.v1.success({ ...endpointDetails.result }); }, }, @@ -68,7 +68,7 @@ API.v1.addRoute( extension: String, }), ); - const endpointDetails = await Voip.getRegistrationInfo(this.queryParams); + const endpointDetails = await VoipAsterisk.getRegistrationInfo(this.queryParams); const encKey = settings.get('VoIP_JWT_Secret'); if (!encKey) { logger.warn('No JWT keys set. Sending registration info as plain text'); @@ -111,7 +111,7 @@ API.v1.addRoute( return API.v1.notFound('Extension not found'); } - const endpointDetails = await Voip.getRegistrationInfo({ extension }); + const endpointDetails = await VoipAsterisk.getRegistrationInfo({ extension }); const encKey = settings.get('VoIP_JWT_Secret'); if (!encKey) { logger.warn('No JWT keys set. Sending registration info as plain text'); diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index e1ee82d724789..e2128375ea42b 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -3,6 +3,7 @@ import type { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typin import { Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; import { API } from '../../api'; import { getPaginationItems } from '../../helpers/getPaginationItems'; import { logger } from './logger'; @@ -79,6 +80,15 @@ API.v1.addRoute( try { await Users.setExtension(user._id, extension); + + void notifyOnUserChange({ + clientAction: 'updated', + id: user._id, + diff: { + extension, + }, + }); + return API.v1.success(); } catch (e) { logger.error({ msg: 'Extension already in use' }); @@ -150,6 +160,15 @@ API.v1.addRoute( logger.debug(`Removing extension association for user ${user._id} (extension was ${user.extension})`); await Users.unsetExtension(user._id); + + void notifyOnUserChange({ + clientAction: 'updated', + id: user._id, + diff: { + extension: null, + }, + }); + return API.v1.success(); }, }, diff --git a/apps/meteor/app/api/server/v1/voip/queues.ts b/apps/meteor/app/api/server/v1/voip/queues.ts index f55dbbadda5fc..4a8fb86be0e39 100644 --- a/apps/meteor/app/api/server/v1/voip/queues.ts +++ b/apps/meteor/app/api/server/v1/voip/queues.ts @@ -1,4 +1,4 @@ -import { Voip } from '@rocket.chat/core-services'; +import { VoipAsterisk } from '@rocket.chat/core-services'; import type { IVoipConnectorResult, IQueueSummary, IQueueMembershipDetails, IQueueMembershipSubscription } from '@rocket.chat/core-typings'; import { Match, check } from 'meteor/check'; @@ -9,7 +9,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, { async get() { - const queueSummary = await Voip.getQueueSummary(); + const queueSummary = await VoipAsterisk.getQueueSummary(); return API.v1.success({ summary: queueSummary.result as IQueueSummary[] }); }, }, @@ -26,7 +26,7 @@ API.v1.addRoute( extension: String, }), ); - const membershipDetails: IVoipConnectorResult = await Voip.getQueuedCallsForThisExtension(this.queryParams); + const membershipDetails: IVoipConnectorResult = await VoipAsterisk.getQueuedCallsForThisExtension(this.queryParams); return API.v1.success(membershipDetails.result as IQueueMembershipDetails); }, }, @@ -43,7 +43,7 @@ API.v1.addRoute( extension: String, }), ); - const membershipDetails: IVoipConnectorResult = await Voip.getQueueMembership(this.queryParams); + const membershipDetails: IVoipConnectorResult = await VoipAsterisk.getQueueMembership(this.queryParams); return API.v1.success(membershipDetails.result as IQueueMembershipSubscription); }, }, diff --git a/apps/meteor/app/api/server/v1/voip/server-connection.ts b/apps/meteor/app/api/server/v1/voip/server-connection.ts index 77e6267640f3c..1bf2b85e8c7a4 100644 --- a/apps/meteor/app/api/server/v1/voip/server-connection.ts +++ b/apps/meteor/app/api/server/v1/voip/server-connection.ts @@ -1,4 +1,4 @@ -import { Voip } from '@rocket.chat/core-services'; +import { VoipAsterisk } from '@rocket.chat/core-services'; import { Match, check } from 'meteor/check'; import { API } from '../../api'; @@ -18,7 +18,7 @@ API.v1.addRoute( }), ); const { host, port, username, password } = this.queryParams; - return API.v1.success(await Voip.checkManagementConnection(host, port, username, password)); + return API.v1.success(await VoipAsterisk.checkManagementConnection(host, port, username, password)); }, }, ); @@ -53,7 +53,7 @@ API.v1.addRoute( } } - return API.v1.success(await Voip.checkCallserverConnection(socketUrl)); + return API.v1.success(await VoipAsterisk.checkCallserverConnection(socketUrl)); }, }, ); diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 5e018c51de897..5ffef64730519 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -111,8 +111,8 @@ export class AppCommandsBridge extends CommandBridge { permission: command.permission, callback: this._appCommandExecutor.bind(this), providesPreview: command.providesPreview, - previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), - previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as + previewer: command.providesPreview ? this._appCommandPreviewer.bind(this) : undefined, + previewCallback: (command.providesPreview ? this._appCommandPreviewExecutor.bind(this) : undefined) as | (typeof slashCommands.commands)[string]['previewCallback'] | undefined, } as SlashCommand; @@ -155,10 +155,6 @@ export class AppCommandsBridge extends CommandBridge { if (typeof command.providesPreview !== 'boolean') { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } - - if (typeof command.executor !== 'function') { - throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); - } } private async _appCommandExecutor({ command, message, params, triggerId, userId }: SlashCommandCallbackParams): Promise { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index 1535a18823c54..9d62769336a25 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -72,55 +72,51 @@ export class AppHttpBridge extends HttpBridge { this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info); - try { - const response = await fetch( - url.href, - { - method, - body: content, - headers, - timeout, - }, - (request.hasOwnProperty('strictSSL') && !request.strictSSL) || - (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), - ); - - const result: IHttpResponse = { - url: info.url, - method: info.method, - statusCode: response.status, - headers: Object.fromEntries(response.headers as unknown as any), - }; - - const body = Buffer.from(await response.arrayBuffer()); - - if (request.encoding === null) { - /** - * The property `content` is not appropriately typed in the - * Apps-engine definition, and we can't simply change it there - * as it would be a breaking change. Thus, we're left with this - * type assertion. - */ - result.content = body as any; - } else { - result.content = body.toString(request.encoding as BufferEncoding); - result.data = ((): any => { - const contentType = (response.headers.get('content-type') || '').split(';')[0]; - if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { - return null; - } - - try { - return JSON.parse(result.content); - } catch { - return null; - } - })(); - } - - return result; - } catch (e: any) { - return e.response; + const response = await fetch( + url.href, + { + method, + body: content, + headers, + timeout, + }, + (request.hasOwnProperty('strictSSL') && !request.strictSSL) || + (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), + ); + + const result: IHttpResponse = { + url: info.url, + method: info.method, + statusCode: response.status, + headers: Object.fromEntries(response.headers as unknown as any), + }; + + const body = Buffer.from(await response.arrayBuffer()); + + if (request.encoding === null) { + /** + * The property `content` is not appropriately typed in the + * Apps-engine definition, and we can't simply change it there + * as it would be a breaking change. Thus, we're left with this + * type assertion. + */ + result.content = body as any; + } else { + result.content = body.toString(request.encoding as BufferEncoding); + result.data = ((): any => { + const contentType = (response.headers.get('content-type') || '').split(';')[0]; + if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { + return null; + } + + try { + return JSON.parse(result.content); + } catch { + return null; + } + })(); } + + return result; } } diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index e90171813df85..37803d4f94f3c 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -3,6 +3,8 @@ import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; + export class AppSettingBridge extends ServerSettingBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); @@ -54,7 +56,7 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${setting.id}" is not readable.`); } - await Settings.updateValueById(setting.id, setting.value); + (await Settings.updateValueById(setting.id, setting.value)).modifiedCount && void notifyOnSettingChangedById(setting.id); } protected async incrementValue(id: string, value: number, appId: string): Promise { @@ -64,6 +66,9 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${id}" is not readable.`); } - await Settings.incrementValueById(id, value); + const { value: setting } = await Settings.incrementValueById(id, value, { returnDocument: 'after' }); + if (setting) { + void notifyOnSettingChanged(setting); + } } } diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index b0dfedd6273bc..13d2436c768c9 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -11,6 +11,7 @@ import { deleteUser } from '../../../lib/server/functions/deleteUser'; import { getUserCreatedByApp } from '../../../lib/server/functions/getUserCreatedByApp'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; +import { notifyOnUserChange, notifyOnUserChangeById } from '../../../lib/server/lib/notifyListener'; export class AppUserBridge extends UserBridge { constructor(private readonly orch: IAppServerOrchestrator) { @@ -97,6 +98,8 @@ export class AppUserBridge extends UserBridge { throw new Error('Creating normal users is currently not supported'); } + void notifyOnUserChangeById({ clientAction: 'inserted', id: user._id }); + return user._id; } @@ -137,6 +140,8 @@ export class AppUserBridge extends UserBridge { await Users.updateOne({ _id: user.id }, { $set: fields as any }); + void notifyOnUserChange({ clientAction: 'updated', id: user.id, diff: fields }); + return true; } diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index ba3d70ccc3c4b..e89bf71a04281 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -35,6 +35,7 @@ export class AppUsersConverter { name: user.name, roles: user.roles, status: user.status, + statusText: user.statusText, statusConnection, utcOffset: user.utcOffset, createdAt: user.createdAt, diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 0acba139d5f44..b9653a2f1b9a9 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -13,6 +13,7 @@ import sharp from 'sharp'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { RocketChatFile } from '../../file/server'; import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../lib/server/lib/notifyListener'; import { settings, settingsRegistry } from '../../settings/server'; import { getExtension } from '../../utils/lib/mimeTypes'; import { getURL } from '../../utils/server/getURL'; @@ -261,7 +262,13 @@ class RocketChatAssetsClass { defaultUrl: assetInstance.defaultUrl, }; - void Settings.updateValueById(key, value); + void (async () => { + const { modifiedCount } = await Settings.updateValueById(key, value); + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + })(); + return RocketChatAssets.processAsset(key, value); }, 200); }); @@ -282,7 +289,13 @@ class RocketChatAssetsClass { defaultUrl: getAssetByKey(asset).defaultUrl, }; - void Settings.updateValueById(key, value); + void (async () => { + const { modifiedCount } = await Settings.updateValueById(key, value); + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + })(); + await RocketChatAssets.processAsset(key, value); } @@ -371,7 +384,8 @@ export async function addAssetToSetting(asset: string, value: IRocketChatAsset, if (currentValue && typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; - await Settings.updateValueById(key, currentValue); + + (await Settings.updateValueById(key, currentValue)).modifiedCount && void notifyOnSettingChangedById(key); } } diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index cc5a04c275b7b..bffbe1f9876dd 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -19,6 +19,7 @@ import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUser import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; import { joinDefaultChannels } from '../../../lib/server/functions/joinDefaultChannels'; import { setAvatarFromServiceWithValidation } from '../../../lib/server/functions/setUserAvatar'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; @@ -323,7 +324,8 @@ const insertUserDocAsync = async function (options, user) { if (!roles.includes('admin') && !hasAdmin) { roles.push('admin'); if (settings.get('Show_Setup_Wizard') === 'pending') { - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + (await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount && + void notifyOnSettingChangedById('Show_Setup_Wizard'); } } } diff --git a/apps/meteor/app/authorization/client/index.ts b/apps/meteor/app/authorization/client/index.ts index dd335c13030ea..7dc1a3466f49a 100644 --- a/apps/meteor/app/authorization/client/index.ts +++ b/apps/meteor/app/authorization/client/index.ts @@ -1,4 +1,5 @@ import { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } from './hasPermission'; import { hasRole, hasAnyRole } from './hasRole'; +import './restrictedRoles'; export { hasAllPermission, hasAtLeastOnePermission, hasRole, hasAnyRole, hasPermission, userHasAllPermission }; diff --git a/apps/meteor/app/authorization/client/restrictedRoles.ts b/apps/meteor/app/authorization/client/restrictedRoles.ts new file mode 100644 index 0000000000000..5aa5e426c2bda --- /dev/null +++ b/apps/meteor/app/authorization/client/restrictedRoles.ts @@ -0,0 +1,12 @@ +import { Meteor } from 'meteor/meteor'; + +import { sdk } from '../../utils/client/lib/SDKClient'; +import { AuthorizationUtils } from '../lib'; + +Meteor.startup(async () => { + const result = await sdk.call('license:isEnterprise'); + if (result) { + // #ToDo: Load this from the server with an API call instead of having a duplicate list + AuthorizationUtils.addRolePermissionWhiteList('guest', ['view-d-room', 'view-joined-room', 'view-p-room', 'start-discussion']); + } +}); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 91769e71270a1..7cd953a52bb2a 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -34,7 +34,7 @@ export const canDeleteMessageAsync = async ( if (!allowed) { return false; } - const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', rid); if (!bypassBlockTimeLimit) { const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts index cb6422a031425..13a114732bd28 100644 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -41,11 +42,15 @@ Meteor.methods({ action: 'Adding_permission', }); } + // for setting-based-permissions, authorize the group access as well if (permission.groupPermissionId) { await Permissions.addRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } await Permissions.addRole(permission._id, role); + + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts index 30a1b2a759b60..91a4df1eddf75 100644 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -36,10 +37,12 @@ Meteor.methods({ // for setting based permissions, revoke the group permission once all setting permissions // related to this group have been removed - if (permission.groupPermissionId) { await Permissions.removeRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } + await Permissions.removeRole(permission._id, role); + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts rename to apps/meteor/app/canned-responses/client/collections/CannedResponse.ts diff --git a/apps/meteor/ee/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/index.ts rename to apps/meteor/app/canned-responses/client/index.ts diff --git a/apps/meteor/ee/app/canned-responses/client/startup/responses.js b/apps/meteor/app/canned-responses/client/startup/responses.js similarity index 84% rename from apps/meteor/ee/app/canned-responses/client/startup/responses.js rename to apps/meteor/app/canned-responses/client/startup/responses.js index 6d5834d91cc00..5959452832619 100644 --- a/apps/meteor/ee/app/canned-responses/client/startup/responses.js +++ b/apps/meteor/app/canned-responses/client/startup/responses.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { hasPermission } from '../../../../../app/authorization/client'; -import { settings } from '../../../../../app/settings/client'; -import { sdk } from '../../../../../app/utils/client/lib/SDKClient'; +import { hasPermission } from '../../../authorization/client'; +import { settings } from '../../../settings/client'; +import { sdk } from '../../../utils/client/lib/SDKClient'; import { CannedResponse } from '../collections/CannedResponse'; const events = { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index 8aa8dc4578a52..0fc15f878bcf9 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -8,6 +8,7 @@ import type { Document, UpdateResult } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; +import { notifyOnIntegrationChangedByChannels } from '../../../lib/server/lib/notifyListener'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => { @@ -73,10 +74,13 @@ export async function saveRoomName( if (room.name && !isDiscussion) { await Integrations.updateRoomName(room.name, slugifiedRoomName); + void notifyOnIntegrationChangedByChannels([slugifiedRoomName]); } + if (sendMessage) { await Message.saveSystemMessage('r', rid, displayName, user); } + await callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name }); return displayName; } diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index b1845511c7b79..e17faebea384d 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -10,6 +10,7 @@ import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; +import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; import { saveReactWhenReadOnly } from '../functions/saveReactWhenReadOnly'; import { saveRoomAnnouncement } from '../functions/saveRoomAnnouncement'; import { saveRoomCustomFields } from '../functions/saveRoomCustomFields'; @@ -116,14 +117,10 @@ const validators: RoomSettingsValidators = { } }, async retentionEnabled({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.enabled) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.enabled) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -131,14 +128,10 @@ const validators: RoomSettingsValidators = { } }, async retentionMaxAge({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.maxAge) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.maxAge) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -146,14 +139,10 @@ const validators: RoomSettingsValidators = { } }, async retentionExcludePinned({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.excludePinned) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.excludePinned) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -161,14 +150,10 @@ const validators: RoomSettingsValidators = { } }, async retentionFilesOnly({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.filesOnly) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.filesOnly) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -176,14 +161,10 @@ const validators: RoomSettingsValidators = { } }, async retentionIgnoreThreads({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.ignoreThreads) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.ignoreThreads) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -468,7 +449,7 @@ export async function saveRoomSettings( rid, }); - if (setting === 'retentionOverrideGlobal') { + if (setting === 'retentionOverrideGlobal' && settings.retentionOverrideGlobal === false) { delete settings.retentionMaxAge; delete settings.retentionExcludePinned; delete settings.retentionFilesOnly; @@ -487,6 +468,8 @@ export async function saveRoomSettings( }); } + void notifyOnRoomChangedById(rid); + return { result: true, rid: room._id, diff --git a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts index 14cb2f4a57cef..0550a3d7f238e 100644 --- a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts +++ b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts @@ -1,6 +1,7 @@ import { Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { userScopes } from '../oauthScopes'; import { getRedirectUri } from './getRedirectUri'; @@ -10,6 +11,8 @@ export async function getOAuthAuthorizationUrl() { await Settings.updateValueById('Cloud_Workspace_Registration_State', state); + void notifyOnSettingChangedById('Cloud_Workspace_Registration_State'); + const cloudUrl = settings.get('Cloud_Url'); const clientId = settings.get('Cloud_Workspace_Client_Id'); const redirectUri = getRedirectUri(); diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 7e970edfdfc23..1ea20812c0621 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -1,5 +1,6 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; @@ -32,11 +33,13 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save const accessToken = await getWorkspaceAccessTokenWithScope(scope, throwOnError); if (save) { - await Promise.all([ - Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token), - Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt), - ]); + (await Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Access_Token'); + + (await Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Access_Token_Expires_At'); } + return accessToken.token; } diff --git a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts index b4e5362f5ac7a..45e1738e11e62 100644 --- a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts +++ b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts @@ -1,5 +1,6 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; export async function removeWorkspaceRegistrationInfo() { @@ -8,16 +9,30 @@ export async function removeWorkspaceRegistrationInfo() { return true; } - await Promise.all([ - Settings.resetValueById('Cloud_Workspace_Id', null), - Settings.resetValueById('Cloud_Workspace_Name', null), - Settings.resetValueById('Cloud_Workspace_Client_Id', null), - Settings.resetValueById('Cloud_Workspace_Client_Secret', null), - Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At', null), - Settings.resetValueById('Cloud_Workspace_PublicKey', null), - Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri', null), - ]); + const settingsIds = [ + 'Cloud_Workspace_Id', + 'Cloud_Workspace_Name', + 'Cloud_Workspace_Client_Id', + 'Cloud_Workspace_Client_Secret', + 'Cloud_Workspace_Client_Secret_Expires_At', + 'Cloud_Workspace_PublicKey', + 'Cloud_Workspace_Registration_Client_Uri', + 'Show_Setup_Wizard', + ]; + + const promises = settingsIds.map((settingId) => { + if (settingId === 'Show_Setup_Wizard') { + return Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + } + + return Settings.resetValueById(settingId, null); + }); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]); + } + }); - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); return true; } diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts index cb2d19cfb92ed..d922733024426 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts @@ -1,6 +1,7 @@ import { applyLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { syncCloudData } from './syncWorkspace/syncCloudData'; @@ -33,7 +34,8 @@ export async function saveRegistrationData({ await syncCloudData(); } -function saveRegistrationDataBase({ + +async function saveRegistrationDataBase({ workspaceId, client_name, client_id, @@ -50,38 +52,47 @@ function saveRegistrationDataBase({ publicKey: string; registration_client_uri: string; }) { - return Promise.all([ - Settings.updateValueById('Register_Server', true), - Settings.updateValueById('Cloud_Workspace_Id', workspaceId), - Settings.updateValueById('Cloud_Workspace_Name', client_name), - Settings.updateValueById('Cloud_Workspace_Client_Id', client_id), - Settings.updateValueById('Cloud_Workspace_Client_Secret', client_secret), - 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), - ]).then(async (...results) => { - // wait until all the settings are updated before syncing the data - for await (const retry of Array.from({ length: 10 })) { - if ( - settings.get('Register_Server') === true && - settings.get('Cloud_Workspace_Id') === workspaceId && - settings.get('Cloud_Workspace_Name') === client_name && - settings.get('Cloud_Workspace_Client_Id') === client_id && - settings.get('Cloud_Workspace_Client_Secret') === client_secret && - settings.get('Cloud_Workspace_Client_Secret_Expires_At') === client_secret_expires_at && - settings.get('Cloud_Workspace_PublicKey') === publicKey && - settings.get('Cloud_Workspace_Registration_Client_Uri') === registration_client_uri - ) { - break; - } + const settingsData = [ + { _id: 'Register_Server', value: true }, + { _id: 'Cloud_Workspace_Id', value: workspaceId }, + { _id: 'Cloud_Workspace_Name', value: client_name }, + { _id: 'Cloud_Workspace_Client_Id', value: client_id }, + { _id: 'Cloud_Workspace_Client_Secret', value: client_secret }, + { _id: 'Cloud_Workspace_Client_Secret_Expires_At', value: client_secret_expires_at }, + { _id: 'Cloud_Workspace_PublicKey', value: publicKey }, + { _id: 'Cloud_Workspace_Registration_Client_Uri', value: registration_client_uri }, + ]; + + const promises = settingsData.map(({ _id, value }) => Settings.updateValueById(_id, value)); - if (retry === 9) { - throw new Error('Failed to save registration data'); - } - await new Promise((resolve) => setTimeout(resolve, 1000)); + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsData[index]._id); } - return results; }); + + // TODO: Why is this taking so long that needs a timeout? + for await (const retry of Array.from({ length: 10 })) { + const isSettingsUpdated = + settings.get('Register_Server') === true && + settings.get('Cloud_Workspace_Id') === workspaceId && + settings.get('Cloud_Workspace_Name') === client_name && + settings.get('Cloud_Workspace_Client_Id') === client_id && + settings.get('Cloud_Workspace_Client_Secret') === client_secret && + settings.get('Cloud_Workspace_Client_Secret_Expires_At') === client_secret_expires_at && + settings.get('Cloud_Workspace_PublicKey') === publicKey && + settings.get('Cloud_Workspace_Registration_Client_Uri') === registration_client_uri; + + if (isSettingsUpdated) { + return; + } + + if (retry === 9) { + throw new Error('Failed to save registration data'); + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } } export async function saveRegistrationDataManual({ diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index 5f5df80d0d3db..1fb2dcc064493 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; @@ -15,7 +16,7 @@ export async function startRegisterWorkspace(resend = false) { return true; } - await Settings.updateValueById('Register_Server', true); + (await Settings.updateValueById('Register_Server', true)).modifiedCount && void notifyOnSettingChangedById('Register_Server'); const regInfo = await buildWorkspaceRegistrationData(undefined); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts index 3493401144cfb..32753ba004298 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts @@ -2,7 +2,7 @@ import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communic export const supportedVersionsChooseLatest = async (...tokens: (SignedSupportedVersions | undefined)[]) => { const [token] = (tokens.filter((r) => r?.timestamp != null) as SignedSupportedVersions[]).sort((a, b) => { - return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); + return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }); return token; diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index 473acef88c29a..f4334bd04d647 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -6,6 +6,7 @@ import type { Response } from '@rocket.chat/server-fetch'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { settings } from '../../../../settings/server'; import { supportedVersions as supportedVersionsFromBuild } from '../../../../utils/rocketchat-supported-versions.info'; import { buildVersionUpdateMessage } from '../../../../version-check/server/functions/buildVersionUpdateMessage'; @@ -62,9 +63,10 @@ const cacheValueInSettings = ( reset: () => Promise; } => { const reset = async () => { + SystemLogger.debug(`Resetting cached value ${key} in settings`); const value = await fn(); - await Settings.updateValueById(key, value); + (await Settings.updateValueById(key, value)).modifiedCount && void notifyOnSettingChangedById(key); return value; }; @@ -134,6 +136,31 @@ const getSupportedVersionsToken = async () => { (response.success && response.result) || undefined, ); + SystemLogger.debug({ + msg: 'Supported versions', + supportedVersionsFromBuild: supportedVersionsFromBuild.timestamp, + versionsFromLicense: versionsFromLicense?.supportedVersions?.timestamp, + response: response.success && response.result?.timestamp, + }); + + switch (supportedVersions) { + case supportedVersionsFromBuild: + SystemLogger.info({ + msg: 'Using supported versions from build', + }); + break; + case versionsFromLicense?.supportedVersions: + SystemLogger.info({ + msg: 'Using supported versions from license', + }); + break; + case response.success && response.result: + SystemLogger.info({ + msg: 'Using supported versions from cloud', + }); + break; + } + await buildVersionUpdateMessage(supportedVersions?.versions); return supportedVersions?.signed; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts index 91202d9731708..f2e66bfdf9f77 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts @@ -5,6 +5,7 @@ import { v, compile } from 'suretype'; import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { settings } from '../../../../settings/server'; import type { WorkspaceRegistrationData } from '../buildRegistrationData'; import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; @@ -126,11 +127,13 @@ const fetchWorkspaceClientPayload = async ({ /** @deprecated */ const consumeWorkspaceSyncPayload = async (result: Serialized) => { if (result.publicKey) { - await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey); + (await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_PublicKey'); } if (result.trial?.trialID) { - await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + (await Settings.updateValueById('Cloud_Workspace_Had_Trial', true)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Had_Trial'); } // add banners diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index e43c65c70f918..70f54dd7b7261 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -10,6 +10,7 @@ import { crowdIntervalValuesToCronMap } from '../../../server/settings/crowd'; import { deleteUser } from '../../lib/server/functions/deleteUser'; import { _setRealName } from '../../lib/server/functions/setRealName'; import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus'; +import { notifyOnUserChange, notifyOnUserChangeById, notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { logger } from './logger'; @@ -215,6 +216,15 @@ export class CROWD { }, ); + void notifyOnUserChange({ + clientAction: 'updated', + id, + diff: { + ...user, + ...(crowdUser.displayname && { name: crowdUser.displayname }), + }, + }); + await setUserActiveStatus(id, crowdUser.active); } @@ -312,6 +322,21 @@ export class CROWD { }, ); + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(crowdUser._id, { projection: { 'services.resume.loginTokens': 1 } }); + if (!userTokens) { + return; + } + + return { + clientAction: 'updated', + id: crowdUser._id, + diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, + }; + }); + await this.syncDataToUser(crowdUser, user._id); return { @@ -324,6 +349,8 @@ export class CROWD { try { crowdUser._id = await Accounts.createUserAsync(crowdUser); + void notifyOnUserChangeById({ clientAction: 'inserted', id: crowdUser._id }); + // sync the user data await this.syncDataToUser(crowdUser, crowdUser._id); diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index 6b225069734d4..16407bf134de4 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -11,6 +11,7 @@ import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; +import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { settings } from '../../settings/server'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; @@ -374,6 +375,8 @@ export class CustomOAuth { }; await Users.update({ _id: user._id }, update); + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: update }); } }); diff --git a/apps/meteor/app/e2e/client/E2EEState.ts b/apps/meteor/app/e2e/client/E2EEState.ts new file mode 100644 index 0000000000000..0e505ec4a1bd3 --- /dev/null +++ b/apps/meteor/app/e2e/client/E2EEState.ts @@ -0,0 +1,9 @@ +export enum E2EEState { + NOT_STARTED = 'NOT_STARTED', + DISABLED = 'DISABLED', + LOADING_KEYS = 'LOADING_KEYS', + READY = 'READY', + SAVE_PASSWORD = 'SAVE_PASSWORD', + ENTER_PASSWORD = 'ENTER_PASSWORD', + ERROR = 'ERROR', +} diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.js index 2e0843ee33801..49b157c5ccf45 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.js @@ -53,6 +53,10 @@ export async function encryptAES(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } +export async function encryptAESCTR(vector, key, data) { + return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); +} + export async function decryptRSA(key, data) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } @@ -65,6 +69,10 @@ export async function generateAESKey() { return crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); } +export async function generateAESCTRKey() { + return crypto.subtle.generateKey({ name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']); +} + export async function generateRSAKey() { return crypto.subtle.generateKey( { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 9e2f72d381158..ac7cedb3fd9d9 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -1,6 +1,5 @@ import { Base64 } from '@rocket.chat/base64'; import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { RoomManager } from '../../../client/lib/RoomManager'; @@ -23,6 +22,8 @@ import { importAESKey, importRSAKey, readFileAsArrayBuffer, + encryptAESCTR, + generateAESCTRKey, } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; @@ -41,6 +42,7 @@ const permitedMutations = { E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS, + E2ERoomState.CREATING_KEYS, ], }; @@ -92,6 +94,14 @@ export class E2ERoom extends Emitter { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } + hasSessionKey() { + return !!this.groupSessionKey; + } + + getState() { + return this.state; + } + setState(requestedState) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -180,20 +190,20 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - const data = await (subscription.lastMessage?.msg && this.decrypt(subscription.lastMessage.msg)); - if (!data?.text) { + if (subscription.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } + const message = await this.decryptMessage(subscription.lastMessage); + Subscriptions.update( { _id: subscription._id, }, { $set: { - 'lastMessage.msg': data.text, - 'lastMessage.e2e': 'done', + lastMessage: message, }, }, ); @@ -208,6 +218,10 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { + if (!e2e.isReady()) { + return; + } + if (this.state !== E2ERoomState.KEYS_RECEIVED && this.state !== E2ERoomState.NOT_STARTED) { return; } @@ -307,17 +321,29 @@ export class E2ERoom extends Emitter { async encryptKeyForOtherParticipants() { // Encrypt generated session key for every user in room and publish to subscription model. try { - const { users } = await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId); - users.forEach((user) => this.encryptForParticipant(user)); + const users = (await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId)).users.filter((user) => user?.e2e?.public_key); + + if (!users.length) { + return; + } + + const usersSuggestedGroupKeys = { [this.roomId]: [] }; + for await (const user of users) { + const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e.public_key); + + usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey }); + } + + await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys }); } catch (error) { return this.error('Error getting room users: ', error); } } - async encryptForParticipant(user) { + async encryptGroupKeyForParticipant(public_key) { let userKey; try { - userKey = await importRSAKey(JSON.parse(user.e2e.public_key), ['encrypt']); + userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -326,76 +352,103 @@ export class E2ERoom extends Emitter { // Encrypt session key for this user with his/her public key try { const encryptedUserKey = await encryptRSA(userKey, toArrayBuffer(this.sessionKeyExportedString)); - // Key has been encrypted. Publish to that user's subscription model for this room. - await sdk.call('e2e.updateGroupKey', this.roomId, user._id, this.keyID + Base64.encode(new Uint8Array(encryptedUserKey))); + const encryptedUserKeyToString = this.keyID + Base64.encode(new Uint8Array(encryptedUserKey)); + return encryptedUserKeyToString; } catch (error) { return this.error('Error encrypting user key: ', error); } } + async sha256Hash(text) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data))); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } + // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return; - } + // if (!this.isSupportedRoomType(this.typeOfRoom)) { + // return; + // } const fileArrayBuffer = await readFileAsArrayBuffer(file); const vector = crypto.getRandomValues(new Uint8Array(16)); + const key = await generateAESCTRKey(); let result; try { - result = await encryptAES(vector, this.groupSessionKey, fileArrayBuffer); + result = await encryptAESCTR(vector, key, fileArrayBuffer); } catch (error) { + console.log(error); return this.error('Error encrypting group key: ', error); } - const output = joinVectorAndEcryptedData(vector, result); + const exportedKey = await window.crypto.subtle.exportKey('jwk', key); + + const fileName = await this.sha256Hash(file.name); - const encryptedFile = new File([toArrayBuffer(EJSON.stringify(output))], file.name); + const encryptedFile = new File([toArrayBuffer(result)], fileName); - return encryptedFile; + return { + file: encryptedFile, + key: exportedKey, + iv: Base64.encode(vector), + type: file.type, + }; } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(message) { - if (message[0] !== '{') { - return; - } - - const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(message)); + async decryptFile(file, key, iv) { + const ivArray = Base64.decode(iv); + const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - try { - return await decryptAES(vector, this.groupSessionKey, cipherText); - } catch (error) { - this.error('Error decrypting file: ', error); - - return false; - } + return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); } // Encrypts messages async encryptText(data) { - if (!(typeof data === 'function' || (typeof data === 'object' && !!data))) { - data = new TextEncoder('UTF-8').encode(EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) })); - } - - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return data; - } - const vector = crypto.getRandomValues(new Uint8Array(16)); - let result; + try { - result = await encryptAES(vector, this.groupSessionKey, data); + const result = await encryptAES(vector, this.groupSessionKey, data); + return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } catch (error) { - return this.error('Error encrypting message: ', error); + this.error('Error encrypting message: ', error); + throw error; } + } + + // Helper function for encryption of content + async encryptMessageContent(contentToBeEncrypted) { + const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); - return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await this.encryptText(data), + }; + } + + // Helper function for encryption of content + async encryptMessage(message) { + const { msg, attachments, ...rest } = message; + + const content = await this.encryptMessageContent({ msg, attachments }); + + return { + ...rest, + content, + t: 'e2e', + e2e: 'pending', + }; } // Helper function for encryption of messages encrypt(message) { + if (!this.isSupportedRoomType(this.typeOfRoom)) { + return; + } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( @@ -410,45 +463,38 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - encryptAttachmentDescription(description, _id) { - const ts = new Date(); + async decryptContent(data) { + if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(data.content.ciphertext); + Object.assign(data, content); + } - const data = new TextEncoder('UTF-8').encode( - EJSON.stringify({ - userId: this.userId, - text: description, - _id, - ts, - }), - ); - return this.encryptText(data); + return data; } // Decrypt messages - async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } - const data = await this.decrypt(message.msg); + if (message.msg) { + const data = await this.decrypt(message.msg); - if (!data?.text) { - return message; + if (data?.text) { + message.msg = data.text; + } } + message = await this.decryptContent(message); + return { ...message, - msg: data.text, e2e: 'done', }; } async decrypt(message) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return message; - } - const keyID = message.slice(0, 12); if (keyID !== this.keyID) { @@ -473,5 +519,27 @@ export class E2ERoom extends Emitter { } this.encryptKeyForOtherParticipants(); + this.setState(E2ERoomState.READY); + } + + onStateChange(cb) { + this.on('STATE_CHANGED', cb); + return () => this.off('STATE_CHANGED', cb); + } + + async encryptGroupKeyForParticipantsWaitingForTheKeys(users) { + if (!this.isReady()) { + return; + } + + const usersWithKeys = await Promise.all( + users.map(async (user) => { + const { _id, public_key } = user; + const key = await this.encryptGroupKeyForParticipant(public_key); + return { _id, key }; + }), + ); + + return usersWithKeys; } } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 472e719599337..0cc344ff51527 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -1,13 +1,13 @@ import QueryString from 'querystring'; import URL from 'url'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isFileAttachment } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, MessageAttachment } from '@rocket.chat/core-typings'; +import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; +import _ from 'lodash'; import { Meteor } from 'meteor/meteor'; -import type { ReactiveVar as ReactiveVarType } from 'meteor/reactive-var'; -import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; import * as banners from '../../../client/lib/banners'; import type { LegacyBannerPayload } from '../../../client/lib/banners'; @@ -19,11 +19,13 @@ import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordMod import SaveE2EPasswordModal from '../../../client/views/e2e/SaveE2EPasswordModal'; import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; import { getMessageUrlRegex } from '../../../lib/getMessageUrlRegex'; +import { isTruthy } from '../../../lib/isTruthy'; import { ChatRoom, Subscriptions, Messages } from '../../models/client'; import { settings } from '../../settings/client'; import { getUserAvatarURL } from '../../utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; +import { E2EEState } from './E2EEState'; import { toString, toArrayBuffer, @@ -40,6 +42,7 @@ import { } from './helper'; import { log, logError } from './logger'; import { E2ERoom } from './rocketchat.e2e.room'; + import './events.js'; let failedToDecodeKey = false; @@ -49,36 +52,58 @@ type KeyPair = { private_key: string | null; }; +const ROOM_KEY_EXCHANGE_SIZE = 10; +const E2EEStateDependency = new Tracker.Dependency(); + class E2E extends Emitter { private started: boolean; - public enabled: ReactiveVarType; - - private _ready: ReactiveVarType; - private instancesByRoomId: Record; - private db_public_key: string | null; + private db_public_key: string | null | undefined; - private db_private_key: string | null; + private db_private_key: string | null | undefined; public privateKey: CryptoKey | undefined; + private keyDistributionInterval: ReturnType | null; + + private state: E2EEState; + + private observable: Meteor.LiveQueryHandle | undefined; + constructor() { super(); this.started = false; - this.enabled = new ReactiveVar(false); - this._ready = new ReactiveVar(false); this.instancesByRoomId = {}; + this.keyDistributionInterval = null; + this.observable = undefined; + + this.on('E2E_STATE_CHANGED', ({ prevState, nextState }) => { + this.log(`${prevState} -> ${nextState}`); + }); - this.on('ready', async () => { - this._ready.set(true); - this.log('startClient -> Done'); - this.log('decryptSubscriptions'); + this.on(E2EEState.READY, async () => { + await this.onE2EEReady(); + }); + + this.on(E2EEState.SAVE_PASSWORD, async () => { + await this.onE2EEReady(); + }); + + this.on(E2EEState.DISABLED, () => { + this.observable?.stop(); + }); - await this.decryptSubscriptions(); - this.log('decryptSubscriptions -> Done'); + this.on(E2EEState.NOT_STARTED, () => { + this.observable?.stop(); }); + + this.on(E2EEState.ERROR, () => { + this.observable?.stop(); + }); + + this.setState(E2EEState.NOT_STARTED); } log(...msg: unknown[]) { @@ -89,12 +114,140 @@ class E2E extends Emitter { logError('E2E', ...msg); } + getState() { + return this.state; + } + isEnabled(): boolean { - return this.enabled.get(); + return this.state !== E2EEState.DISABLED; } isReady(): boolean { - return this.enabled.get() && this._ready.get(); + E2EEStateDependency.depend(); + + // Save_Password state is also a ready state for E2EE + return this.state === E2EEState.READY || this.state === E2EEState.SAVE_PASSWORD; + } + + async onE2EEReady() { + this.log('startClient -> Done'); + this.initiateHandshake(); + await this.handleAsyncE2EESuggestedKey(); + this.log('decryptSubscriptions'); + await this.decryptSubscriptions(); + this.log('decryptSubscriptions -> Done'); + await this.initiateDecryptingPendingMessages(); + this.log('DecryptingPendingMessages -> Done'); + await this.initiateKeyDistribution(); + this.log('initiateKeyDistribution -> Done'); + this.observeSubscriptions(); + this.log('observing subscriptions'); + } + + observeSubscriptions() { + this.observable?.stop(); + + this.observable = Subscriptions.find().observe({ + changed: (sub: ISubscription) => { + setTimeout(async () => { + this.log('Subscription changed', sub); + if (!sub.encrypted && !sub.E2EKey) { + this.removeInstanceByRoomId(sub.rid); + return; + } + + const e2eRoom = await this.getInstanceByRoomId(sub.rid); + if (!e2eRoom) { + return; + } + + if (sub.E2ESuggestedKey) { + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + await this.acceptSuggestedKey(sub.rid); + e2eRoom.keyReceived(); + } else { + console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + await this.rejectSuggestedKey(sub.rid); + } + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + + // Cover private groups and direct messages + if (!e2eRoom.isSupportedRoomType(sub.t)) { + e2eRoom.disable(); + return; + } + + if (sub.E2EKey && e2eRoom.isWaitingKeys()) { + e2eRoom.keyReceived(); + return; + } + + if (!e2eRoom.isReady()) { + return; + } + + await e2eRoom.decryptSubscription(); + }, 0); + }, + added: (sub: ISubscription) => { + setTimeout(async () => { + this.log('Subscription added', sub); + if (!sub.encrypted && !sub.E2EKey) { + return; + } + return this.getInstanceByRoomId(sub.rid); + }, 0); + }, + removed: (sub: ISubscription) => { + this.log('Subscription removed', sub); + this.removeInstanceByRoomId(sub.rid); + }, + }); + } + + shouldAskForE2EEPassword() { + const { private_key } = this.getKeysFromLocalStorage(); + return this.db_private_key && !private_key; + } + + setState(nextState: E2EEState) { + const prevState = this.state; + + this.state = nextState; + + E2EEStateDependency.changed(); + + this.emit('E2E_STATE_CHANGED', { prevState, nextState }); + + this.emit(nextState); + } + + async handleAsyncE2EESuggestedKey() { + const subs = Subscriptions.find({ E2ESuggestedKey: { $exists: true } }).fetch(); + await Promise.all( + subs + .filter((sub) => sub.E2ESuggestedKey && !sub.E2EKey) + .map(async (sub) => { + const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); + + if (!e2eRoom) { + return; + } + + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + this.log('Imported valid E2E suggested key'); + await e2e.acceptSuggestedKey(sub.rid); + e2eRoom.keyReceived(); + } else { + this.error('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + await e2e.rejectSuggestedKey(sub.rid); + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + }), + ); } async getInstanceByRoomId(rid: IRoom['_id']): Promise { @@ -119,7 +272,11 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string): Promise { + async persistKeys( + { public_key, private_key }: KeyPair, + password: string, + { force }: { force: boolean } = { force: false }, + ): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } @@ -133,6 +290,7 @@ class E2E extends Emitter { await sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', { public_key, private_key: encodedPrivateKey, + force, }); } @@ -155,6 +313,35 @@ class E2E extends Emitter { }; } + initiateHandshake() { + Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].handshake()); + } + + async initiateDecryptingPendingMessages() { + await Promise.all(Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].decryptPendingMessages())); + } + + openSaveE2EEPasswordModal(randomPassword: string) { + imperativeModal.open({ + component: SaveE2EPasswordModal, + props: { + randomPassword, + onClose: imperativeModal.close, + onCancel: () => { + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: () => { + Meteor._localStorage.removeItem('e2e.randomPassword'); + this.setState(E2EEState.READY); + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + async startClient(): Promise { if (this.started) { return; @@ -172,9 +359,10 @@ class E2E extends Emitter { public_key = this.db_public_key; } - if (!private_key && this.db_private_key) { + if (this.shouldAskForE2EEPassword()) { try { - private_key = await this.decodePrivateKey(this.db_private_key); + this.setState(E2EEState.ENTER_PASSWORD); + private_key = await this.decodePrivateKey(this.db_private_key as string); } catch (error) { this.started = false; failedToDecodeKey = true; @@ -195,44 +383,29 @@ class E2E extends Emitter { if (public_key && private_key) { await this.loadKeys({ public_key, private_key }); + this.setState(E2EEState.READY); } else { await this.createAndLoadKeys(); + this.setState(E2EEState.READY); } if (!this.db_public_key || !this.db_private_key) { + this.setState(E2EEState.LOADING_KEYS); await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); if (randomPassword) { + this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ title: () => t('Save_your_encryption_password'), html: () => t('Click_here_to_view_and_copy_your_password'), modifiers: ['large'], closable: false, icon: 'key', - action: () => { - imperativeModal.open({ - component: SaveE2EPasswordModal, - props: { - randomPassword, - onClose: imperativeModal.close, - onCancel: () => { - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); - this.closeAlert(); - dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Set') }); - imperativeModal.close(); - }, - }, - }); - }, + action: () => this.openSaveE2EEPasswordModal(randomPassword), }); } - this.emit('ready'); } async stopClient(): Promise { @@ -243,13 +416,14 @@ class E2E extends Emitter { Meteor._localStorage.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; - this.enabled.set(false); - this._ready.set(false); this.started = false; + this.keyDistributionInterval && clearInterval(this.keyDistributionInterval); + this.keyDistributionInterval = null; + this.setState(E2EEState.DISABLED); } async changePassword(newPassword: string): Promise { - await this.persistKeys(this.getKeysFromLocalStorage(), newPassword); + await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); if (Meteor._localStorage.getItem('e2e.randomPassword')) { Meteor._localStorage.setItem('e2e.randomPassword', newPassword); @@ -258,12 +432,17 @@ class E2E extends Emitter { async loadKeysFromDB(): Promise { try { + this.setState(E2EEState.LOADING_KEYS); const { public_key, private_key } = await sdk.rest.get('/v1/e2e.fetchMyKeys'); this.db_public_key = public_key; this.db_private_key = private_key; } catch (error) { - return this.error('Error fetching RSA keys: ', error); + this.setState(E2EEState.ERROR); + this.error('Error fetching RSA keys: ', error); + // Stop any process since we can't communicate with the server + // to get the keys. This prevents new key generation + throw error; } } @@ -275,17 +454,20 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', private_key); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); } } async createAndLoadKeys(): Promise { // Could not obtain public-private keypair from server. + this.setState(E2EEState.LOADING_KEYS); let key; try { key = await generateRSAKey(); this.privateKey = key.privateKey; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error generating key: ', error); } @@ -294,6 +476,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); } @@ -302,6 +485,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); } @@ -327,6 +511,7 @@ class E2E extends Emitter { return EJSON.stringify(joinVectorAndEcryptedData(vector, encodedPrivateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error encrypting encodedPrivateKey: ', error); } } @@ -341,6 +526,7 @@ class E2E extends Emitter { try { baseKey = await importRawKey(toArrayBuffer(password)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error creating a key based on user password: ', error); } @@ -348,30 +534,34 @@ class E2E extends Emitter { try { return await deriveKey(toArrayBuffer(Meteor.userId()), baseKey); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error deriving baseKey: ', error); } } - async requestPassword(): Promise { + openEnterE2EEPasswordModal(onEnterE2EEPassword?: (password: string) => void) { + imperativeModal.open({ + component: EnterE2EPasswordModal, + props: { + onClose: imperativeModal.close, + onCancel: () => { + failedToDecodeKey = false; + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: (password) => { + onEnterE2EEPassword?.(password); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + + async requestPasswordAlert(): Promise { return new Promise((resolve) => { - const showModal = () => { - imperativeModal.open({ - component: EnterE2EPasswordModal, - props: { - onClose: imperativeModal.close, - onCancel: () => { - failedToDecodeKey = false; - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: (password) => { - resolve(password); - this.closeAlert(); - imperativeModal.close(); - }, - }, - }); - }; + const showModal = () => this.openEnterE2EEPasswordModal((password) => resolve(password)); const showAlert = () => { this.openAlert({ @@ -394,8 +584,42 @@ class E2E extends Emitter { }); } + async requestPasswordModal(): Promise { + return new Promise((resolve) => this.openEnterE2EEPasswordModal((password) => resolve(password))); + } + + async decodePrivateKeyFlow() { + const password = await this.requestPasswordModal(); + const masterKey = await this.getMasterKey(password); + + if (!this.db_private_key) { + return; + } + + const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(this.db_private_key)); + + try { + const privKey = await decryptAES(vector, masterKey, cipherText); + const privateKey = toString(privKey) as string; + + if (this.db_public_key && privateKey) { + await this.loadKeys({ public_key: this.db_public_key, private_key: privateKey }); + this.setState(E2EEState.READY); + } else { + await this.createAndLoadKeys(); + this.setState(E2EEState.READY); + } + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + throw new Error('E2E -> Error decrypting private key'); + } + } + async decodePrivateKey(privateKey: string): Promise { - const password = await this.requestPassword(); + const password = await this.requestPasswordAlert(); const masterKey = await this.getMasterKey(password); @@ -405,10 +629,27 @@ class E2E extends Emitter { const privKey = await decryptAES(vector, masterKey, cipherText); return toString(privKey); } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); throw new Error('E2E -> Error decrypting private key'); } } + async decryptFileContent(file: IUploadWithUser): Promise { + if (!file.rid) { + return file; + } + + const e2eRoom = await this.getInstanceByRoomId(file.rid); + + if (!e2eRoom) { + return file; + } + + return e2eRoom.decryptContent(file); + } + async decryptMessage(message: IMessage | IE2EEMessage): Promise { if (!isE2EEMessage(message) || message.e2e === 'done') { return message; @@ -420,27 +661,17 @@ class E2E extends Emitter { return message; } - const data = await e2eRoom.decrypt(message.msg); - - const decryptedMessage: IE2EEMessage = { - ...message, - ...(data && { - msg: data.text, - e2e: 'done', - }), - }; + const decryptedMessage: IE2EEMessage = await e2eRoom.decryptMessage(message); const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); - const decryptedMessageWithAttachments = await this.decryptMessageAttachments(decryptedMessageWithQuote); - - return decryptedMessageWithAttachments; + return decryptedMessageWithQuote; } - async decryptMessageAttachments(message: IMessage): Promise { - const { attachments } = message; + async decryptPinnedMessage(message: IMessage) { + const pinnedMessage = message?.attachments?.[0]?.text; - if (!attachments || !attachments.length) { + if (!pinnedMessage) { return message; } @@ -450,32 +681,16 @@ class E2E extends Emitter { return message; } - const decryptedAttachments = await Promise.all( - attachments.map(async (attachment) => { - if (!isFileAttachment(attachment)) { - return attachment; - } - - if (!attachment.description) { - return attachment; - } - - const data = await e2eRoom.decrypt(attachment.description); + const data = await e2eRoom.decrypt(pinnedMessage); - if (!data) { - return attachment; - } + if (!data) { + return message; + } - attachment.description = data.text; - return attachment; - }), - ); + const decryptedPinnedMessage = { ...message } as IMessage & { attachments: MessageAttachment[] }; + decryptedPinnedMessage.attachments[0].text = data.text; - return { - ...message, - attachments: decryptedAttachments, - e2e: 'done', - }; + return decryptedPinnedMessage; } async decryptPendingMessages(): Promise { @@ -505,6 +720,9 @@ class E2E extends Emitter { } async parseQuoteAttachment(message: IE2EEMessage): Promise { + if (!message?.msg) { + return message; + } const urls = message.msg.match(getMessageUrlRegex()) || []; await Promise.all( @@ -550,6 +768,99 @@ class E2E extends Emitter { return message; } + + async getSuggestedE2EEKeys(usersWaitingForE2EKeys: Record) { + const roomIds = Object.keys(usersWaitingForE2EKeys); + return Object.fromEntries( + ( + await Promise.all( + roomIds.map(async (room) => { + const e2eRoom = await this.getInstanceByRoomId(room); + + if (!e2eRoom) { + return; + } + const usersWithKeys = await e2eRoom.encryptGroupKeyForParticipantsWaitingForTheKeys(usersWaitingForE2EKeys[room]); + + if (!usersWithKeys) { + return; + } + + return [room, usersWithKeys]; + }), + ) + ).filter(isTruthy), + ); + } + + async getSample(roomIds: string[], limit = 3): Promise { + if (limit === 0) { + return []; + } + + const randomRoomIds = _.sampleSize(roomIds, ROOM_KEY_EXCHANGE_SIZE); + + const sampleIds: string[] = []; + for await (const roomId of randomRoomIds) { + const e2eroom = await this.getInstanceByRoomId(roomId); + if (!e2eroom?.hasSessionKey()) { + continue; + } + + sampleIds.push(roomId); + } + + if (!sampleIds.length) { + return this.getSample(roomIds, limit - 1); + } + + return sampleIds; + } + + async initiateKeyDistribution() { + if (this.keyDistributionInterval) { + return; + } + + const keyDistribution = async () => { + const roomIds = ChatRoom.find({ + 'usersWaitingForE2EKeys': { $exists: true }, + 'usersWaitingForE2EKeys.userId': { $ne: Meteor.userId() }, + }).map((room) => room._id); + if (!roomIds.length) { + return; + } + + // Prevent function from running and doing nothing when theres something to do + const sampleIds = await this.getSample(roomIds); + + if (!sampleIds.length) { + return; + } + + const { usersWaitingForE2EKeys = {} } = await sdk.rest.get('/v1/e2e.fetchUsersWaitingForGroupKey', { roomIds: sampleIds }); + + if (!Object.keys(usersWaitingForE2EKeys).length) { + return; + } + + const userKeysWithRooms = await this.getSuggestedE2EEKeys(usersWaitingForE2EKeys); + + if (!Object.keys(userKeysWithRooms).length) { + return; + } + + try { + await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys: userKeysWithRooms }); + } catch (error) { + return this.error('Error providing group key to users: ', error); + } + }; + + // Run first call right away, then schedule for 10s in the future + await keyDistribution(); + this.keyDistributionInterval = setInterval(keyDistribution, 10000); + } } export const e2e = new E2E(); diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts index dcd1f82edbc86..860051c04d4df 100644 --- a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; export async function handleSuggestedGroupKey( @@ -23,6 +23,11 @@ export async function handleSuggestedGroupKey( if (handle === 'accept') { await Subscriptions.setGroupE2EKey(sub._id, suggestedKey); + await Rooms.removeUsersFromE2EEQueueByRoomId(sub.rid, [userId]); + } + + if (handle === 'reject') { + await Rooms.addUserIdToE2EEQueueByRoomIds([sub.rid], userId); } await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); diff --git a/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts new file mode 100644 index 0000000000000..42408f398ecb9 --- /dev/null +++ b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts @@ -0,0 +1,33 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; + +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; + +export const provideUsersSuggestedGroupKeys = async ( + userId: IUser['_id'], + usersSuggestedGroupKeys: Record, +) => { + const roomIds = Object.keys(usersSuggestedGroupKeys); + + if (!roomIds.length) { + return; + } + + // Process should try to process all rooms i have access instead of dying if one is not + for await (const roomId of roomIds) { + if (!(await canAccessRoomIdAsync(roomId, userId))) { + continue; + } + + const usersWithSuggestedKeys = []; + for await (const user of usersSuggestedGroupKeys[roomId]) { + const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(user._id, roomId, user.key); + if (!modifiedCount) { + continue; + } + usersWithSuggestedKeys.push(user._id); + } + + await Rooms.removeUsersFromE2EEQueueByRoomId(roomId, usersWithSuggestedKeys); + } +}; diff --git a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts index 005df0bb2a7a3..6b0e685616b5c 100644 --- a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts +++ b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts @@ -5,6 +5,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,5 +45,7 @@ Meteor.methods({ } await Rooms.setE2eKeyId(room._id, keyID); + + void notifyOnRoomChangedById(room._id); }, }); diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index c00b9b872466a..94d252601bc49 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -1,11 +1,11 @@ -import { Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string }): void; + 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void; } } @@ -19,9 +19,22 @@ Meteor.methods({ }); } + if (!keyPair.force) { + const keys = await Users.fetchKeysByUserId(userId); + + if (keys.private_key && keys.public_key) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } + } + await Users.setE2EPublicAndPrivateKeysByUserId(userId, { private_key: keyPair.private_key, public_key: keyPair.public_key, }); + + const subscribedRoomIds = await Rooms.getSubscribedRoomIdsWithoutE2EKeys(userId); + await Rooms.addUserIdToE2EEQueueByRoomIds(subscribedRoomIds, userId); }, }); diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index 30053cc7164ac..c856f8cf708a6 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -2,6 +2,8 @@ import { Subscriptions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -11,6 +13,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'e2e.updateGroupKey'(rid, uid, key) { + methodDeprecationLogger.method('e2e.updateGroupKey', '8.0.0'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' }); @@ -27,10 +30,7 @@ Meteor.methods({ } // uid also has subscription to this room - const userSub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid); - if (userSub) { - await Subscriptions.setGroupE2ESuggestedKey(userSub._id, key); - } + await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); } }, }); diff --git a/apps/meteor/ee/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/Session.ts rename to apps/meteor/app/ecdh/Session.ts diff --git a/apps/meteor/ee/app/ecdh/client/ClientSession.ts b/apps/meteor/app/ecdh/client/ClientSession.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/client/ClientSession.ts rename to apps/meteor/app/ecdh/client/ClientSession.ts diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 99a7497f13c7f..264443a1378bc 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -3,10 +3,17 @@ import { Meteor } from 'meteor/meteor'; import { throttledCounter } from '../../../../lib/utils/throttledCounter'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; const incException = throttledCounter((counter) => { - Settings.incrementValueById('Uncaught_Exceptions_Count', counter).catch(console.error); + Settings.incrementValueById('Uncaught_Exceptions_Count', counter, { returnDocument: 'after' }) + .then(({ value }) => { + if (value) { + void notifyOnSettingChanged(value); + } + }) + .catch(console.error); }, 10000); class ErrorHandler { diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 4b3e148bbdad1..4cab0b0c41e83 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -7,6 +7,7 @@ import { broadcastMessageFromData } from '../../../../server/modules/watchers/li import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; +import { notifyOnRoomChanged, notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; import { notifyUsersOnMessage } from '../../../lib/server/lib/notifyUsersOnMessage'; import { sendAllNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; import { processThreads } from '../../../threads/server/hooks/aftersavemessage'; @@ -48,12 +49,18 @@ const eventHandlers = { if (persistedRoom) { // Update the federation await Rooms.updateOne({ _id: persistedRoom._id }, { $set: { federation: room.federation } }); + + // Notify watch.rooms listener + void notifyOnRoomChangedById(room._id); } else { // Denormalize room const denormalizedRoom = normalizers.denormalizeRoom(room); // Create the room - await Rooms.insertOne(denormalizedRoom); + const insertedRoom = await Rooms.insertOne(denormalizedRoom); + + // Notify watch.rooms listener + void notifyOnRoomChangedById(insertedRoom.insertedId); } } return eventResult; @@ -74,6 +81,9 @@ const eventHandlers = { if (persistedRoom) { // Delete the room await deleteRoom(roomId); + + // Notify watch.rooms listener + void notifyOnRoomChanged(persistedRoom, 'removed'); } // Remove all room events @@ -145,6 +155,9 @@ const eventHandlers = { // Update the room's federation property await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); + + // Notify watch.rooms listener + void notifyOnRoomChangedById(roomId); } } @@ -171,6 +184,9 @@ const eventHandlers = { // Update the room's federation property await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); + + // Notify watch.rooms listener + void notifyOnRoomChangedById(roomId); } return eventResult; @@ -196,6 +212,9 @@ const eventHandlers = { // Update the room's federation property await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); + + // Notify watch.rooms listener + void notifyOnRoomChangedById(roomId); } return eventResult; @@ -475,6 +494,9 @@ const eventHandlers = { // Mute user await Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username); + + // Broadcast the unmute event + void notifyOnRoomChangedById(roomId); } return eventResult; @@ -497,6 +519,9 @@ const eventHandlers = { // Unmute user await Rooms.unmuteMutedUsernameByRoomId(roomId, denormalizedUser.username); + + // Broadcast the unmute event + void notifyOnRoomChangedById(roomId); } return eventResult; diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts index 3b9090311e011..c684b7b8f74a3 100644 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ b/apps/meteor/app/federation/server/functions/helpers.ts @@ -2,6 +2,7 @@ import { isDirectMessageRoom } from '@rocket.chat/core-typings'; import type { ISubscription, IUser, IRoom } from '@rocket.chat/core-typings'; import { Settings, Users, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; export const getNameAndDomain = (fullyQualifiedName: string): string[] => fullyQualifiedName.split('@'); @@ -14,11 +15,12 @@ export async function isRegisteringOrEnabled(): Promise { } export async function updateStatus(status: string): Promise { + // No need to call ws listener because current function is called on startup await Settings.updateValueById('FEDERATION_Status', status); } export async function updateEnabled(enabled: boolean): Promise { - await Settings.updateValueById('FEDERATION_Enabled', enabled); + (await Settings.updateValueById('FEDERATION_Enabled', enabled)).modifiedCount && void notifyOnSettingChangedById('FEDERATION_Enabled'); } export const checkRoomType = (room: IRoom): boolean => room.t === 'p' || room.t === 'd'; diff --git a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts index e0887888c478d..c8e6f90966fd4 100644 --- a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts +++ b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts @@ -5,7 +5,9 @@ import { Tracker } from 'meteor/tracker'; Tracker.autorun(() => { const userId = Meteor.userId(); - if (userId) { + // Check for Meteor.loggingIn to be reactive and ensure it will process only after login finishes + // preventing race condition setting the rc_token as null forever + if (userId && Meteor.loggingIn() === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${escape(userId)}; path=/${secure}`; diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 567e5e5d71ebf..0f551d3b90d1c 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -1,6 +1,5 @@ import http from 'http'; import https from 'https'; -import URL from 'url'; import _ from 'underscore'; @@ -8,12 +7,12 @@ import { settings } from '../../../settings/server'; import type { S3Options } from '../../ufs/AmazonS3/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server'; +import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { - const { query } = URL.parse(req.url || '', true); - const forceDownload = typeof query.download !== 'undefined'; + const forcedDownload = forceDownload(req); - const fileUrl = await this.store.getRedirectURL(file, forceDownload); + const fileUrl = await this.store.getRedirectURL(file, forcedDownload); if (!fileUrl || !file.store) { res.end(); return; @@ -23,7 +22,7 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, if (settings.get(`FileUpload_S3_Proxy_${storeType}`)) { const request = /^https:/.test(fileUrl) ? https : http; - FileUpload.proxyFile(file.name || '', fileUrl, forceDownload, request, req, res); + FileUpload.proxyFile(file.name || '', fileUrl, forcedDownload, request, req, res); return; } diff --git a/apps/meteor/app/file-upload/server/config/FileSystem.ts b/apps/meteor/app/file-upload/server/config/FileSystem.ts index 98342daf2e46d..75fdb5afc8aeb 100644 --- a/apps/meteor/app/file-upload/server/config/FileSystem.ts +++ b/apps/meteor/app/file-upload/server/config/FileSystem.ts @@ -4,6 +4,7 @@ import { UploadFS } from '../../../../server/ufs'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; +import { getContentDisposition } from './helper'; const FileSystemUploads = new FileUploadClass({ name: 'FileSystem:Uploads', @@ -26,7 +27,8 @@ const FileSystemUploads = new FileUploadClass({ } file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); + + res.setHeader('Content-Disposition', `${getContentDisposition(req)}; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); file.uploadedAt && res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); res.setHeader('Content-Type', file.type || 'application/octet-stream'); diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index 41eb4350b8768..8fb901b5a1235 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -1,18 +1,17 @@ import http from 'http'; import https from 'https'; -import URL from 'url'; import _ from 'underscore'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/GoogleStorage/server'; +import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { - const { query } = URL.parse(req.url || '', true); - const forceDownload = typeof query.download !== 'undefined'; + const forcedDownload = forceDownload(req); - const fileUrl = await this.store.getRedirectURL(file, forceDownload); + const fileUrl = await this.store.getRedirectURL(file, forcedDownload); if (!fileUrl || !file.store) { res.end(); return; @@ -22,7 +21,7 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, if (settings.get(`FileUpload_GoogleStorage_Proxy_${storeType}`)) { const request = /^https:/.test(fileUrl) ? https : http; - FileUpload.proxyFile(file.name || '', fileUrl, forceDownload, request, req, res); + FileUpload.proxyFile(file.name || '', fileUrl, forcedDownload, request, req, res); return; } diff --git a/apps/meteor/app/file-upload/server/config/GridFS.ts b/apps/meteor/app/file-upload/server/config/GridFS.ts index 629d177581bfc..3bb5f806f3a75 100644 --- a/apps/meteor/app/file-upload/server/config/GridFS.ts +++ b/apps/meteor/app/file-upload/server/config/GridFS.ts @@ -9,6 +9,7 @@ import { Logger } from '@rocket.chat/logger'; import { UploadFS } from '../../../../server/ufs'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; +import { getContentDisposition } from './helper'; const logger = new Logger('FileUpload'); @@ -161,7 +162,7 @@ new FileUploadClass({ async get(file, req, res) { file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); + res.setHeader('Content-Disposition', `${getContentDisposition(req)}; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); file.uploadedAt && res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); res.setHeader('Content-Type', file.type || 'application/octet-stream'); res.setHeader('Content-Length', file.size || 0); diff --git a/apps/meteor/app/file-upload/server/config/helper.ts b/apps/meteor/app/file-upload/server/config/helper.ts new file mode 100644 index 0000000000000..f1c465537255c --- /dev/null +++ b/apps/meteor/app/file-upload/server/config/helper.ts @@ -0,0 +1,21 @@ +import type http from 'http'; +import URL from 'url'; + +export const forceDownload = (req: http.IncomingMessage): boolean => { + const { query } = URL.parse(req.url || '', true); + + const forceDownload = typeof query.download !== 'undefined'; + if (forceDownload) { + return true; + } + + return query.contentDisposition === 'attachment'; +}; + +export const getContentDisposition = (req: http.IncomingMessage): string => { + const { query } = URL.parse(req.url || '', true); + if (query.contentDisposition === 'inline') { + return 'inline'; + } + return 'attachment'; +}; diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 4458f9d618813..08e2ccb0a52be 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -28,7 +28,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { UploadFS } from '../../../../server/ufs'; import { ufsComplete } from '../../../../server/ufs/ufs-methods'; import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store'; -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { settings } from '../../../settings/server'; import { mime } from '../../../utils/lib/mimeTypes'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; @@ -170,7 +170,7 @@ export const FileUpload = { throw new Meteor.Error('error-file-too-large', reason); } - if (!fileUploadIsValidContentType(file.type as string, '')) { + if (!fileUploadIsValidContentType(file?.type)) { const reason = i18n.t('File_type_is_not_accepted', { lng: language }); throw new Meteor.Error('error-invalid-file-type', reason); } @@ -420,7 +420,6 @@ export const FileUpload = { await Avatars.deleteFile(oldAvatar._id); } await Avatars.updateFileNameById(file._id, user.username); - // console.log('upload finished ->', file); }, async requestCanAccessFiles({ headers = {}, url }: http.IncomingMessage, file?: IUpload) { @@ -464,16 +463,26 @@ export const FileUpload = { return false; } - if (!settings.get('FileUpload_Restrict_to_room_members') || !file?.rid) { + if (!file?.rid) { return true; } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + const fileUploadRestrictedToMembers = settings.get('FileUpload_Restrict_to_room_members'); + const fileUploadRestrictToUsersWhoCanAccessRoom = settings.get('FileUpload_Restrict_to_users_who_can_access_room'); - if (subscription) { + if (!fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { return true; } + if (fileUploadRestrictedToMembers && !fileUploadRestrictToUsersWhoCanAccessRoom) { + const sub = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + return !!sub; + } + + if (fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { + return canAccessRoomIdAsync(file.rid, user._id); + } + return false; }, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 08e3225ad9a1c..485528a5e62fe 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -159,6 +159,13 @@ export const sendFileMessage = async ( file: Partial; msgData?: Record; }, + { + parseAttachmentsForE2EE, + }: { + parseAttachmentsForE2EE: boolean; + } = { + parseAttachmentsForE2EE: true, + }, ): Promise => { const user = await Users.findOneById(userId); if (!user) { @@ -187,23 +194,32 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - e2e: Match.Optional(String), + content: Match.Optional( + Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), + ), }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - - const msg = await executeSendMessage(userId, { + const data = { rid: roomId, ts: new Date(), - file: files[0], - files, - attachments, ...(msgData as Partial), ...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }), msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, - }); + }; + + if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + data.file = files[0]; + data.files = files; + data.attachments = attachments; + } + + const msg = await executeSendMessage(userId, data); callbacks.runAsync('afterFileUpload', { user, room, message: msg }); diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index 302aeb882ac59..60c07c3288ce0 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -7,6 +7,7 @@ import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class CsvImporter extends Importer { private csvParser: (csv: string) => string[]; @@ -236,7 +237,10 @@ export class CsvImporter extends Importer { } if (usersCount) { - await Settings.incrementValueById('CSV_Importer_Count', usersCount); + const { value } = await Settings.incrementValueById('CSV_Importer_Count', usersCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } } // Check if any of the message usernames was not in the imported list of users diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js index ac3d278d82abc..663300e44154e 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js @@ -6,6 +6,7 @@ import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { Importer, ProgressStep } from '../../importer/server'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; /** @deprecated HipChat was discontinued at 2019-02-15 */ export class HipChatEnterpriseImporter extends Importer { @@ -54,7 +55,11 @@ export class HipChatEnterpriseImporter extends Importer { this.converter.addUser(newUser); } - await Settings.incrementValueById('Hipchat_Enterprise_Importer_Count', count); + const { value } = await Settings.incrementValueById('Hipchat_Enterprise_Importer_Count', count, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + await super.updateRecord({ 'count.users': count }); await super.addCountToTotal(count); } diff --git a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index 2c26531bd5c41..95461820bf2da 100644 --- a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -9,6 +9,7 @@ import { Importer, ProgressStep } from '../../importer/server'; import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class SlackUsersImporter extends Importer { private csvParser: (csv: string) => string[]; @@ -93,7 +94,12 @@ export class SlackUsersImporter extends Importer { await super.updateProgress(ProgressStep.USER_SELECTION); await super.addCountToTotal(userCount); - await Settings.incrementValueById('Slack_Users_Importer_Count', userCount); + + const { value } = await Settings.incrementValueById('Slack_Users_Importer_Count', userCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + await super.updateRecord({ 'count.users': userCount }); return super.getProgress(); } diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index 0ef81c69a1e06..344db66565310 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -4,6 +4,7 @@ import type { IZipEntry } from 'adm-zip'; import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; @@ -337,7 +338,10 @@ export class SlackImporter extends Importer { } if (userCount) { - await Settings.incrementValueById('Slack_Importer_Count', userCount); + const { value } = await Settings.incrementValueById('Slack_Importer_Count', userCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } } const missedTypes: Record = {}; diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 493d14061bf23..7b1e71eaa0f03 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -28,6 +28,7 @@ import { generateUsernameSuggestion } from '../../../lib/server/functions/getUse import { insertMessage } from '../../../lib/server/functions/insertMessage'; import { saveUserIdentity } from '../../../lib/server/functions/saveUserIdentity'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; @@ -250,6 +251,9 @@ export class ImportDataConverter { async updateUser(existingUser: IUser, userData: IImportUser): Promise { const { _id } = existingUser; + if (!_id) { + return; + } userData._id = _id; @@ -297,10 +301,12 @@ export class ImportDataConverter { // Deleted users are 'inactive' users in Rocket.Chat if (userData.deleted && existingUser?.active) { - userData._id && (await setUserActiveStatus(userData._id, false, true)); + await setUserActiveStatus(_id, false, true); } else if (userData.deleted === false && existingUser?.active === false) { - userData._id && (await setUserActiveStatus(userData._id, true)); + await setUserActiveStatus(_id, true); } + + void notifyOnUserChange({ clientAction: 'updated', id: _id, diff: updateData.$set }); } private async hashPassword(password: string): Promise { diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 68a12513a06c6..846f9ef4b4f54 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -7,6 +7,7 @@ import type { MatchKeysAndValues, MongoServerError } from 'mongodb'; import { Selection, SelectionChannel, SelectionUser } from '..'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { t } from '../../../utils/lib/i18n'; import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; import type { ImporterInfo } from '../definitions/ImporterInfo'; @@ -245,10 +246,20 @@ export class Importer { } async applySettingValues(settingValues: OldSettings) { - await Settings.updateValueById('Accounts_AllowUsernameChange', settingValues.allowUsernameChange ?? true); - await Settings.updateValueById('FileUpload_MaxFileSize', settingValues.maxFileSize ?? -1); - await Settings.updateValueById('FileUpload_MediaTypeWhiteList', settingValues.mediaTypeWhiteList ?? '*'); - await Settings.updateValueById('FileUpload_MediaTypeBlackList', settingValues.mediaTypeBlackList ?? ''); + const settingsIds = [ + { _id: 'Accounts_AllowUsernameChange', value: settingValues.allowUsernameChange ?? true }, + { _id: 'FileUpload_MaxFileSize', value: settingValues.maxFileSize ?? -1 }, + { _id: 'FileUpload_MediaTypeWhiteList', value: settingValues.mediaTypeWhiteList ?? '*' }, + { _id: 'FileUpload_MediaTypeBlackList', value: settingValues.mediaTypeBlackList ?? '' }, + ]; + + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); } getProgress(): ImporterProgress { diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 07f7a3d903a24..7a2992f915101 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; +import { notifyOnIntegrationChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { outgoingEvents } from '../../lib/outgoingEvents'; import { outgoingLogger } from '../logger'; @@ -579,6 +580,7 @@ class RocketChatIntegrationHandler { await updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); await Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } }); + void notifyOnIntegrationChangedById(trigger._id); return; } diff --git a/apps/meteor/app/integrations/server/lib/updateHistory.ts b/apps/meteor/app/integrations/server/lib/updateHistory.ts index ed304403e8c75..e8068ad82ac17 100644 --- a/apps/meteor/app/integrations/server/lib/updateHistory.ts +++ b/apps/meteor/app/integrations/server/lib/updateHistory.ts @@ -1,8 +1,8 @@ import type { IIntegrationHistory, OutgoingIntegrationEvent, IIntegration, IMessage, AtLeast } from '@rocket.chat/core-typings'; import { IntegrationHistory } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; import { omit } from '../../../../lib/utils/omit'; +import { notifyOnIntegrationHistoryChangedById, notifyOnIntegrationHistoryChanged } from '../../../lib/server/lib/notifyListener'; export const updateHistory = async ({ historyId, @@ -77,7 +77,12 @@ export const updateHistory = async ({ }; if (historyId) { - await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); + // Projecting just integration field to comply with existing listener behaviour + const integrationHistory = await IntegrationHistory.updateById(historyId, history, { projection: { 'integration._id': 1 } }); + if (!integrationHistory) { + throw new Error('error-updating-integration-history'); + } + void notifyOnIntegrationHistoryChanged(integrationHistory, 'updated', history); return historyId; } @@ -86,11 +91,15 @@ export const updateHistory = async ({ throw new Error('error-invalid-integration'); } - history._createdAt = new Date(); + // TODO: Had to force type cast here because of function's signature + // It would be easier if we separate into create and update functions + const { insertedId } = await IntegrationHistory.create(history as IIntegrationHistory); - const _id = Random.id(); + if (!insertedId) { + throw new Error('error-creating-integration-history'); + } - await IntegrationHistory.insertOne({ _id, ...history } as IIntegrationHistory); + void notifyOnIntegrationHistoryChangedById(insertedId, 'inserted'); - return _id; + return insertedId; }; diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index 2447683bd2914..5b8f13ef1a3ad 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -41,6 +41,7 @@ Meteor.methods({ }); } + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 45548a17a5659..db058bec960bb 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -155,9 +156,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn await Roles.addUserRoles(user._id, ['bot']); - const result = await Integrations.insertOne(integrationData); + const { insertedId } = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 06fb3e3485e33..e73a46bb27db8 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,6 +35,7 @@ export const deleteIncomingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 5358e3233ce7c..0ea5028130da0 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -164,7 +165,7 @@ Meteor.methods({ await Roles.addUserRoles(user._id, ['bot']); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -190,6 +191,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + void notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 59879f99d475c..c8dc31e084466 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -5,6 +5,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -58,8 +59,13 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu const integrationData = await validateOutgoingIntegration(integration, userId); - const result = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + const { insertedId } = await Integrations.insertOne(integrationData); + + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 27750bca50f2e..c9f2211d835b5 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -40,7 +41,9 @@ export const deleteOutgoingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 9e62561ebf9af..116dbd043039c 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -5,6 +5,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -66,7 +67,7 @@ Meteor.methods({ const isFrozen = isScriptEngineFrozen(scriptEngine); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -110,6 +111,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + await notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts index 1c00671ae41d4..87b8133e34917 100644 --- a/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts +++ b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts @@ -3,6 +3,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -53,7 +54,11 @@ export const sendInvitationEmail = async (userId: string, emails: string[]) => { }, }); - await Settings.incrementValueById('Invitation_Email_Count'); + const { value } = await Settings.incrementValueById('Invitation_Email_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + continue; } catch ({ message }: any) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 9ab6c47987d03..09b7a3568362c 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -7,6 +7,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterLogoutCleanUpCallback } from '../../../../lib/callbacks/afterLogoutCleanUpCallback'; import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as servers from '../servers'; import * as localCommandHandlers from './localHandlers'; import * as peerCommandHandlers from './peerHandlers'; @@ -19,15 +20,13 @@ const updateLastPing = withThrottling({ wait: 10_000 })(() => { if (removed) { return; } - void Settings.updateOne( - { _id: 'IRC_Bridge_Last_Ping' }, - { - $set: { - value: new Date(), - }, - }, - { upsert: true }, - ); + + void (async () => { + const updatedValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(), { upsert: true }); + if (updatedValue.modifiedCount || updatedValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } + })(); }); class Bridge { diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js index ed6d635a5721e..3429a977fd1a6 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleQUIT(args) { const user = await Users.findOne({ 'profile.irc.nick': args.nick, @@ -13,4 +15,6 @@ export default async function handleQUIT(args) { }, }, ); + + void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'offline' } }); } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js index fe5df0c9540e5..96a8ebcb3dc90 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleNickChanged(args) { const user = await Users.findOne({ 'profile.irc.nick': args.nick, @@ -21,4 +23,6 @@ export default async function handleNickChanged(args) { }, }, ); + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { name: args.newNick } }); } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js index b4279ae008b4e..5e04d7b79407b 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleUserRegistered(args) { // Check if there is an user with the given username let user = await Users.findOne({ @@ -28,6 +30,8 @@ export default async function handleUserRegistered(args) { }; user = await Users.create(userToInsert); + + void notifyOnUserChange({ id: user._id, clientAction: 'inserted', data: user }); } else { // ...otherwise, log the user in and update the information this.log(`Logging in ${args.username} with nick: ${args.nick}`); @@ -43,5 +47,7 @@ export default async function handleUserRegistered(args) { }, }, ); + + void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'online' } }); } } diff --git a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts index 1bbc5b6a3ef82..24eef975d5d54 100644 --- a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts +++ b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import Bridge from '../irc-bridge'; @@ -16,29 +17,15 @@ Meteor.methods({ async resetIrcConnection() { const ircEnabled = Boolean(settings.get('IRC_Enabled')); - await Settings.updateOne( - { _id: 'IRC_Bridge_Last_Ping' }, - { - $set: { - value: new Date(0), - }, - }, - { - upsert: true, - }, - ); + const updatedLastPingValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(0), { upsert: true }); + if (updatedLastPingValue.modifiedCount || updatedLastPingValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } - await Settings.updateOne( - { _id: 'IRC_Bridge_Reset_Time' }, - { - $set: { - value: new Date(), - }, - }, - { - upsert: true, - }, - ); + const updatedResetTimeValue = await Settings.updateValueById('IRC_Bridge_Reset_Time', new Date(), { upsert: true }); + if (updatedResetTimeValue.modifiedCount || updatedResetTimeValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } if (!ircEnabled) { return { diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 1fc80c6f3a44c..57ea20f00cb1e 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -9,7 +9,9 @@ import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; export const addUserToRoom = async function ( rid: string, @@ -85,6 +87,8 @@ export const addUserToRoom = async function ( ...getDefaultSubscriptionPref(userToBeAdded as IUser), }); + void notifyOnRoomChangedById(rid); + if (!userToBeAdded.username) { throw new Meteor.Error('error-invalid-user', 'Cannot add an user to a room without a username'); } @@ -129,5 +133,9 @@ export const addUserToRoom = async function ( await Team.addMember(inviter || userToBeAdded, userToBeAdded._id, room.teamId); } + if (room.encrypted && settings.get('E2E_Enable') && userToBeAdded.e2e?.public_key) { + await Rooms.addUserIdToE2EEQueueByRoomIds([room._id], userToBeAdded._id); + } + return true; }; diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts index 01bb882aa1445..3378d69f99ff4 100644 --- a/apps/meteor/app/lib/server/functions/archiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/archiveRoom.ts @@ -3,11 +3,18 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnRoomChanged } from '../lib/notifyListener'; export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.archiveById(rid); await Subscriptions.archiveByRoomId(rid); await Message.saveSystemMessage('room-archived', rid, '', user); - await callbacks.run('afterRoomArchived', await Rooms.findOneById(rid), user); + const room = await Rooms.findOneById(rid); + + await callbacks.run('afterRoomArchived', room, user); + + if (room) { + void notifyOnRoomChanged(room); + } }; diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 133eba555a696..2bfb1086c6357 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -4,6 +4,7 @@ import { Messages, Rooms, Subscriptions, ReadReceipts, Users } from '@rocket.cha import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; import { deleteRoom } from './deleteRoom'; export async function cleanRoomHistory({ @@ -110,10 +111,12 @@ export async function cleanRoomHistory({ } if (count) { - const lastMessage = await Messages.getLastVisibleMessageSentWithNoTypeByRoomId(rid); + const lastMessage = await Messages.getLastVisibleUserMessageSentByRoomId(rid); await Rooms.resetLastMessageById(rid, lastMessage, -count); + void notifyOnRoomChangedById(rid); + void api.broadcast('notify.deleteMessageBulk', rid, { rid, excludePinned, @@ -123,5 +126,6 @@ export async function cleanRoomHistory({ ids: selectedMessageIds, }); } + return count; } diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index c1de81332543d..67c6328f38f40 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -11,6 +11,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; const generateSubscription = ( fname: string, @@ -130,6 +131,8 @@ export async function createDirectRoom( // @ts-expect-error - TODO: room expects `u` to be passed, but it's not part of the original object in here const rid = room?._id || (await Rooms.insertOne(roomInfo)).insertedId; + void notifyOnRoomChangedById(rid, isNewRoom ? 'inserted' : 'updated'); + if (roomMembers.length === 1) { // dm to yourself await Subscriptions.updateOne( diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 2d0b94198d96d..19e5fb2f94896 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -12,6 +12,7 @@ import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreate import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; +import { notifyOnRoomChanged } from '../lib/notifyListener'; import { createDirectRoom } from './createDirectRoom'; const isValidName = (name: unknown): name is string => { @@ -117,6 +118,7 @@ export const createRoom = async ( } > => { const { teamId, ...extraData } = roomExtraData || ({} as IRoom); + await beforeCreateRoomCallback.run({ type, // name, @@ -124,9 +126,9 @@ export const createRoom = async ( // members, // readOnly, extraData, - // options, }); + if (type === 'd') { return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username }); } @@ -226,6 +228,8 @@ export const createRoom = async ( const room = await Rooms.createWithFullRoomData(roomProps); + void notifyOnRoomChanged(room, 'inserted'); + const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':'); await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 9368787bf7ea2..e977874b34540 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -9,6 +9,7 @@ import { broadcastMessageFromData } from '../../../../server/modules/watchers/li import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; export const deleteMessageValidatingPermission = async (message: AtLeast, userId: IUser['_id']): Promise => { if (!message?._id) { @@ -80,7 +81,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { await FileUpload.removeFilesByRoomId(rid); @@ -11,4 +12,6 @@ export const deleteRoom = async function (rid: string): Promise { await FileUpload.getStore('Avatars').deleteByRoomId(rid); await callbacks.run('afterDeleteRoom', rid); await Rooms.removeById(rid); + + void notifyOnRoomChangedById(rid, 'removed'); }; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 24ef854d48f3a..d6457664671ad 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -19,6 +19,12 @@ import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; +import { + notifyOnRoomChangedById, + notifyOnIntegrationChangedByUserId, + notifyOnLivechatDepartmentAgentChanged, + notifyOnUserChange, +} from '../lib/notifyListener'; import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -89,11 +95,29 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await Rooms.updateGroupDMsRemovingUsernamesByUsername(user.username, userId); // Remove direct rooms with the user await Rooms.removeDirectRoomContainingUsername(user.username); // Remove direct rooms with the user + const rids = subscribedRooms.map((room) => room.rid); + void notifyOnRoomChangedById(rids); + await Subscriptions.removeByUserId(userId); // Remove user subscriptions + // Remove user as livechat agent if (user.roles.includes('livechat-agent')) { - // Remove user as livechat agent - await LivechatDepartmentAgents.removeByAgentId(userId); + const departmentAgents = await LivechatDepartmentAgents.findByAgentId(userId).toArray(); + + const { deletedCount } = await LivechatDepartmentAgents.removeByAgentId(userId); + + if (deletedCount > 0) { + departmentAgents.forEach((depAgent) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: depAgent._id, + agentId: userId, + departmentId: depAgent.departmentId, + }, + 'removed', + ); + }); + } } if (user.roles.includes('livechat-monitor')) { @@ -110,7 +134,9 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await FileUpload.getStore('Avatars').deleteByName(user.username); } - await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + // Disables all the integrations which rely on the user being deleted. + await Integrations.disableByUserId(userId); + void notifyOnIntegrationChangedByUserId(userId); // Don't broadcast user.deleted for Erasure Type of 'Keep' so that messages don't disappear from logged in sessions if (messageErasureType === 'Delete') { @@ -135,5 +161,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele // Refresh the servers list await FederationServers.refreshServers(); + void notifyOnUserChange({ clientAction: 'removed', id: user._id }); + await callbacks.run('afterDeleteUser', user); } diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 2f6b7a1f694dd..fee7061cae963 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -1,7 +1,8 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -29,7 +30,9 @@ export async function loadMessageHistory({ throw new Error('error-invalid-room'); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: FindOptions = { sort: { diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index dfc6a1716703a..3757c81ee2d75 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -43,7 +43,7 @@ async function getEmailContent({ message, user, room }) { let messageContent = escapeHTML(message.msg); if (message.t === 'e2e') { - messageContent = i18n.t('Encrypted_message', { lng }); + messageContent = i18n.t('Encrypted_message_preview_unavailable', { lng }); } message = await callbacks.run('renderMessage', message); diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 1cc8c4ad54329..3b065c68f15c4 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -7,6 +7,8 @@ import { Meteor } from 'meteor/meteor'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback'; +import { settings } from '../../../settings/server'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; export const removeUserFromRoom = async function ( rid: string, @@ -64,8 +66,14 @@ export const removeUserFromRoom = async function ( await Team.removeMember(room.teamId, user._id); } + if (room.encrypted && settings.get('E2E_Enable')) { + await Rooms.removeUsersFromE2EEQueueByRoomId(room._id, [user._id]); + } + // TODO: CACHE: maybe a queue? await afterLeaveRoomCallback.run(user, room); + void notifyOnRoomChangedById(rid); + await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user); }; diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 3a2808b4171cc..1931333038b61 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -16,6 +16,7 @@ import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { validateEmailDomain } from '../lib'; import { generatePassword } from '../lib/generatePassword'; +import { notifyOnUserChangeById, notifyOnUserChange } from '../lib/notifyListener'; import { passwordPolicy } from '../lib/passwordPolicy'; import { checkEmailAvailability } from './checkEmailAvailability'; import { checkUsernameAvailability } from './checkUsernameAvailability'; @@ -329,6 +330,8 @@ const saveNewUser = async function (userData, sendPassword) { } } + void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); + return _id; }; @@ -401,6 +404,7 @@ export const saveUser = async function (userId, userData) { const updateUser = { $set: {}, + $unset: {}, }; handleBio(updateUser, userData.bio); @@ -419,6 +423,9 @@ export const saveUser = async function (userId, userData) { if (typeof userData.requirePasswordChange !== 'undefined') { updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + if (!userData.requirePasswordChange) { + updateUser.$unset.requirePasswordChangeReason = 1; + } } if (typeof userData.verified === 'boolean') { @@ -428,7 +435,7 @@ export const saveUser = async function (userId, userData) { await Users.updateOne({ _id: userData._id }, updateUser); // App IPostUserUpdated event hook - const userUpdated = await Users.findOneById(userId); + const userUpdated = await Users.findOneById(userData._id); await callbacks.run('afterSaveUser', { user: userUpdated, @@ -445,5 +452,17 @@ export const saveUser = async function (userId, userData) { await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); } + if (typeof userData.verified === 'boolean') { + delete userData.verified; + } + void notifyOnUserChange({ + clientAction: 'updated', + id: userData._id, + diff: { + ...userData, + emails: userUpdated.emails, + }, + }); + return true; }; diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 34ca0ca246db3..0b9ff21e53e3f 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -3,6 +3,7 @@ import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptio import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; +import { notifyOnRoomChangedByUsernamesOrUids } from '../lib/notifyListener'; import { _setRealName } from './setRealName'; import { _setUsername } from './setUsername'; import { updateGroupDMsName } from './updateGroupDMsName'; @@ -134,6 +135,8 @@ async function updateUsernameReferences({ await Subscriptions.setUserUsernameByUserId(user._id, username); await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); + + void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]); } // update other references if either the name or username has changed diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index e167b372b008b..d850f69a93a40 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,5 +1,5 @@ import { Apps } from '@rocket.chat/apps'; -import { Message, api } from '@rocket.chat/core-services'; +import { api, Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -11,6 +11,7 @@ import { broadcastMessageFromData } from '../../../../server/modules/watchers/li import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; @@ -289,12 +290,13 @@ export const sendMessage = async function (user: any, message: any, room: any, u void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageSent', message); } - /* Defer other updates as their return is not interesting to the user */ - - // Execute all callbacks await callbacks.run('afterSaveMessage', message, room); + void broadcastMessageFromData({ id: message._id, }); + + void notifyOnRoomChangedById(message.rid); + return message; }; diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 2d99fb427a46a..e3104db280dd1 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; +import { notifyOnRoomChangedById, notifyOnRoomChangedByUserDM, notifyOnUserChange } from '../lib/notifyListener'; import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; @@ -38,6 +39,7 @@ async function reactivateDirectConversations(userId: string) { }, []); await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); + void notifyOnRoomChangedById(roomsToReactivate); } export async function setUserActiveStatus(userId: string, active: boolean, confirmRelinquish = false): Promise { @@ -105,10 +107,16 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi if (active === false) { await Users.unsetLoginTokens(userId); await Rooms.setDmReadOnlyByUserId(userId, undefined, true, false); + + void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [], active } }); + void notifyOnRoomChangedByUserDM(userId); } else { await Users.unsetReason(userId); + + void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { active } }); await reactivateDirectConversations(userId); } + if (active && !settings.get('Accounts_Send_Email_When_Activating')) { return true; } diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 319202cefea4f..e19ef874db0fd 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -10,6 +10,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { RateLimiter } from '../lib'; +import { notifyOnUserChange } from '../lib/notifyListener'; import { addUserToRoom } from './addUserToRoom'; import { checkUsernameAvailability } from './checkUsernameAvailability'; import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser'; @@ -67,6 +68,8 @@ export const setUsernameWithValidation = async (userId: string, username: string await joinDefaultChannels(user._id, joinDefaultChannelsSilenced); setImmediate(async () => callbacks.run('afterCreateUser', user)); } + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { username } }); }; export const _setUsername = async function (userId: string, u: string, fullUser: IUser): Promise { diff --git a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts index f068827d5c7c1..7db86ed933a37 100644 --- a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts @@ -2,8 +2,12 @@ import { Message } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; + export const unarchiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.unarchiveById(rid); await Subscriptions.unarchiveByRoomId(rid); await Message.saveSystemMessage('room-unarchived', rid, '', user); + + void notifyOnRoomChangedById(rid); }; diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 8fdfc964db4e3..2954517fb0184 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages'; import { settings } from '../../../settings/server'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; @@ -93,12 +94,21 @@ export const updateMessage = async function ( setImmediate(async () => { const msg = await Messages.findOneById(_id); - if (msg) { - await callbacks.run('afterSaveMessage', msg, room, user._id); - void broadcastMessageFromData({ - id: msg._id, - data: msg, - }); + if (!msg) { + return; + } + + // although this is an "afterSave" kind callback, we know they can extend message's properties + // so we wait for it to run before broadcasting + const data = await callbacks.run('afterSaveMessage', msg, room, user._id); + + void broadcastMessageFromData({ + id: msg._id, + data: data as any, // TODO move "afterSaveMessage" type definition to specify a return value + }); + + if (room?.lastMessage?._id === msg._id) { + void notifyOnRoomChangedById(message.rid); } }); }; diff --git a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts index 74b31b63da6a0..08f52620e0805 100644 --- a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts +++ b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts @@ -1,26 +1,10 @@ import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; -import { settings } from '../../../settings/server'; - -const hideMessagesOfTypeServer = new Set(); - -settings.watch('Hide_System_Messages', (values) => { - if (!values || !Array.isArray(values)) { - return; - } - - const hiddenTypes = values.reduce((array, value): MessageTypesValues[] => { +export const getHiddenSystemMessages = (room: IRoom, hiddenSystemMessages: MessageTypesValues[]): MessageTypesValues[] => { + const hiddenTypes = hiddenSystemMessages.reduce((array, value): MessageTypesValues[] => { const newValue: MessageTypesValues[] = value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]; - return [...array, ...newValue]; }, [] as MessageTypesValues[]); - hideMessagesOfTypeServer.clear(); - - hiddenTypes.forEach((item) => hideMessagesOfTypeServer.add(item)); -}); - -// TODO probably remove on chained event system -export function getHiddenSystemMessages(room: IRoom): MessageTypesValues[] { - return Array.isArray(room?.sysMes) ? room.sysMes : [...hideMessagesOfTypeServer]; -} + return Array.isArray(room?.sysMes) ? room.sysMes : hiddenTypes; +}; diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts new file mode 100644 index 0000000000000..f4e948390c99b --- /dev/null +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -0,0 +1,503 @@ +import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; +import type { + IRocketChatRecord, + IRoom, + ILoginServiceConfiguration, + ISetting, + IRole, + IPermission, + IIntegration, + IPbxEvent, + LoginServiceConfiguration as LoginServiceConfigurationData, + ILivechatInquiryRecord, + ILivechatPriority, + ILivechatDepartmentAgents, + IEmailInbox, + IIntegrationHistory, + AtLeast, + ISettingColor, + IUser, +} from '@rocket.chat/core-typings'; +import { + Rooms, + Permissions, + Settings, + PbxEvents, + Roles, + Integrations, + LoginServiceConfiguration, + IntegrationHistory, + LivechatInquiry, + LivechatDepartmentAgents, + Users, +} from '@rocket.chat/models'; + +type ClientAction = 'inserted' | 'updated' | 'removed'; + +export async function notifyOnLivechatPriorityChanged( + data: Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const { _id, ...rest } = data; + + void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } }); +} + +export async function notifyOnRoomChanged( + data: T | T[], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Array.isArray(data) ? data : [data]; + + for (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +} + +export async function notifyOnRoomChangedById( + ids: T['_id'] | T['_id'][], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const eligibleIds = Array.isArray(ids) ? ids : [ids]; + + const items = Rooms.findByIds(eligibleIds); + + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +} + +export async function notifyOnRoomChangedByUsernamesOrUids( + uids: T['u']['_id'][], + usernames: T['u']['username'][], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Rooms.findByUsernamesOrUids(uids, usernames); + + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +} + +export async function notifyOnRoomChangedByUserDM( + userId: T['u']['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Rooms.findDMsByUids([userId]); + + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +} + +export async function notifyOnPermissionChanged(permission: IPermission, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('permission.changed', { clientAction, data: permission }); + + if (permission.level === 'settings' && permission.settingId) { + const setting = await Settings.findOneNotHiddenById(permission.settingId); + if (!setting) { + return; + } + void notifyOnSettingChanged(setting, 'updated'); + } +} + +export async function notifyOnPermissionChangedById(pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + const permission = await Permissions.findOneById(pid); + if (!permission) { + return; + } + + return notifyOnPermissionChanged(permission, clientAction); +} + +export async function notifyOnPbxEventChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await PbxEvents.findOneById(id); + if (!item) { + return; + } + + void api.broadcast('watch.pbxevents', { clientAction, id, data: item }); +} + +export async function notifyOnRoleChanged(role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.roles', { clientAction, role }); +} + +export async function notifyOnRoleChangedById( + id: T['_id'], + clientAction: 'removed' | 'changed' = 'changed', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const role = await Roles.findOneById(id); + if (!role) { + return; + } + + void notifyOnRoleChanged(role, clientAction); +} + +export async function notifyOnLoginServiceConfigurationChanged( + service: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.loginServiceConfiguration', { + clientAction, + id: service._id, + data: service, + }); +} + +export async function notifyOnLoginServiceConfigurationChangedByService( + service: T['service'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await LoginServiceConfiguration.findOneByService>(service, { + projection: { secret: 0 }, + }); + if (!item) { + return; + } + + void notifyOnLoginServiceConfigurationChanged(item, clientAction); +} + +export async function notifyOnIntegrationChanged(data: T, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrations', { clientAction, id: data._id, data }); +} + +export async function notifyOnIntegrationChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await Integrations.findOneById(id); + if (!item) { + return; + } + + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); +} + +export async function notifyOnIntegrationChangedByUserId( + id: T['userId'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByUserId(id); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnIntegrationChangedByChannels( + channels: T['channel'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByChannels(channels); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnEmailInboxChanged( + data: Pick | T, // TODO: improve typing + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data }); +} + +export async function notifyOnLivechatInquiryChanged( + data: ILivechatInquiryRecord | ILivechatInquiryRecord[], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Array.isArray(data) ? data : [data]; + + for (const item of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff }); + } +} + +export async function notifyOnLivechatInquiryChangedById( + id: ILivechatInquiryRecord['_id'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = clientAction === 'removed' ? await LivechatInquiry.trashFindOneById(id) : await LivechatInquiry.findOneById(id); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnLivechatInquiryChangedByRoom( + rid: ILivechatInquiryRecord['rid'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = await LivechatInquiry.findOneByRoomId(rid, {}); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnLivechatInquiryChangedByToken( + token: ILivechatInquiryRecord['v']['token'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = await LivechatInquiry.findOneByToken(token); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnIntegrationHistoryChanged( + data: AtLeast, + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff }); +} + +export async function notifyOnIntegrationHistoryChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await IntegrationHistory.findOneById(id); + + if (!item) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff }); +} + +export async function notifyOnLivechatDepartmentAgentChanged( + data: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data }); +} + +export async function notifyOnLivechatDepartmentAgentChangedByDepartmentId( + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } }); + + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId( + agentsIds: T['agentId'][], + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, { + projection: { _id: 1, agentId: 1, departmentId: 1 }, + }); + + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnSettingChanged( + setting: ISetting & { editor?: ISettingColor['editor'] }, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + void api.broadcast('watch.settings', { clientAction, setting }); +} + +export async function notifyOnSettingChangedById(id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id); + + if (!item) { + return; + } + + void api.broadcast('watch.settings', { clientAction, setting: item }); +} + +type NotifyUserChange = { + id: IUser['_id']; + clientAction: 'inserted' | 'removed' | 'updated'; + data?: IUser; + diff?: Record; + unset?: Record; +}; + +export async function notifyOnUserChange({ clientAction, id, data, diff, unset }: NotifyUserChange) { + if (!dbWatchersDisabled) { + return; + } + if (clientAction === 'removed') { + void api.broadcast('watch.users', { clientAction, id }); + return; + } + if (clientAction === 'inserted') { + void api.broadcast('watch.users', { clientAction, id, data: data! }); + return; + } + + void api.broadcast('watch.users', { clientAction, diff: diff!, unset: unset || {}, id }); +} + +/** + * Calls the callback only if DB Watchers are disabled + */ +export async function notifyOnUserChangeAsync(cb: () => Promise) { + if (!dbWatchersDisabled) { + return; + } + + const result = await cb(); + if (!result) { + return; + } + + if (Array.isArray(result)) { + result.forEach((n) => notifyOnUserChange(n)); + return; + } + + return notifyOnUserChange(result); +} + +// TODO this may be only useful on 'inserted' +export async function notifyOnUserChangeById({ clientAction, id }: { id: IUser['_id']; clientAction: 'inserted' | 'removed' | 'updated' }) { + if (!dbWatchersDisabled) { + return; + } + const user = await Users.findOneById(id); + if (!user) { + return; + } + + void notifyOnUserChange({ id, clientAction, data: user }); +} diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 3c68fb7a2bf29..00ff016395931 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; @@ -7,6 +7,7 @@ import _ from 'underscore'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -67,7 +68,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: Record = { sort: { diff --git a/apps/meteor/app/lib/server/methods/removeOAuthService.ts b/apps/meteor/app/lib/server/methods/removeOAuthService.ts index 6d1bb688979d9..6e16dc8d2d5b5 100644 --- a/apps/meteor/app/lib/server/methods/removeOAuthService.ts +++ b/apps/meteor/app/lib/server/methods/removeOAuthService.ts @@ -5,6 +5,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChangedById } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -31,37 +32,46 @@ Meteor.methods({ name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); name = capitalize(name); - await Promise.all([ - Settings.removeById(`Accounts_OAuth_Custom-${name}`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-url`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-token_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-identity_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-authorize_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-scope`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-access_token_param`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-token_sent_via`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-identity_token_sent_via`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-id`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-secret`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_label_text`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_label_color`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_color`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-login_style`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-key_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-username_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-email_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-name_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-avatar_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-roles_claim`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_roles`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-roles_to_sync`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_users`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-show_button`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-groups_claim`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-channels_admin`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-map_channels`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-groups_channel_map`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`), - ]); + + const settingsIds = [ + `Accounts_OAuth_Custom-${name}`, + `Accounts_OAuth_Custom-${name}-url`, + `Accounts_OAuth_Custom-${name}-token_path`, + `Accounts_OAuth_Custom-${name}-identity_path`, + `Accounts_OAuth_Custom-${name}-authorize_path`, + `Accounts_OAuth_Custom-${name}-scope`, + `Accounts_OAuth_Custom-${name}-access_token_param`, + `Accounts_OAuth_Custom-${name}-token_sent_via`, + `Accounts_OAuth_Custom-${name}-identity_token_sent_via`, + `Accounts_OAuth_Custom-${name}-id`, + `Accounts_OAuth_Custom-${name}-secret`, + `Accounts_OAuth_Custom-${name}-button_label_text`, + `Accounts_OAuth_Custom-${name}-button_label_color`, + `Accounts_OAuth_Custom-${name}-button_color`, + `Accounts_OAuth_Custom-${name}-login_style`, + `Accounts_OAuth_Custom-${name}-key_field`, + `Accounts_OAuth_Custom-${name}-username_field`, + `Accounts_OAuth_Custom-${name}-email_field`, + `Accounts_OAuth_Custom-${name}-name_field`, + `Accounts_OAuth_Custom-${name}-avatar_field`, + `Accounts_OAuth_Custom-${name}-roles_claim`, + `Accounts_OAuth_Custom-${name}-merge_roles`, + `Accounts_OAuth_Custom-${name}-roles_to_sync`, + `Accounts_OAuth_Custom-${name}-merge_users`, + `Accounts_OAuth_Custom-${name}-show_button`, + `Accounts_OAuth_Custom-${name}-groups_claim`, + `Accounts_OAuth_Custom-${name}-channels_admin`, + `Accounts_OAuth_Custom-${name}-map_channels`, + `Accounts_OAuth_Custom-${name}-groups_channel_map`, + `Accounts_OAuth_Custom-${name}-merge_users_distinct_services`, + ]; + + const promises = settingsIds.map((id) => Settings.removeById(id)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.deletedCount) { + void notifyOnSettingChangedById(settingsIds[index], 'removed'); + } + }); }, }); diff --git a/apps/meteor/app/lib/server/methods/saveSetting.ts b/apps/meteor/app/lib/server/methods/saveSetting.ts index d6cd62dc74709..7f900d1751d82 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.ts +++ b/apps/meteor/app/lib/server/methods/saveSetting.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -56,7 +57,10 @@ Meteor.methods({ break; } - await Settings.updateValueAndEditorById(_id, value as SettingValue, editor); + (await Settings.updateValueAndEditorById(_id, value as SettingValue, editor)).modifiedCount && + setting && + void notifyOnSettingChanged({ ...setting, editor, value: value as SettingValue }); + return true; }), }); diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts index 6d7c0927c3f89..8c4f92cfb88f0 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ b/apps/meteor/app/lib/server/methods/saveSettings.ts @@ -9,6 +9,7 @@ import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; +import { notifyOnSettingChangedById } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -81,8 +82,15 @@ Meteor.methods({ case 'boolean': check(value, Boolean); break; + case 'timespan': case 'int': check(value, Number); + if (!Number.isInteger(value)) { + throw new Meteor.Error(`Invalid setting value ${value}`, 'Invalid setting value', { + method: 'saveSettings', + }); + } + break; case 'multiSelect': check(value, Array); @@ -107,7 +115,13 @@ Meteor.methods({ }); } - await Promise.all(params.map(({ _id, value }) => Settings.updateValueById(_id, value))); + const promises = params.map(({ _id, value }) => Settings.updateValueById(_id, value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(params[index]._id); + } + }); return true; }, diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index e12ebc2d47e92..a490b5c4c67f6 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -81,14 +81,23 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { + if (message.t !== 'e2e') { + throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', { + method: 'sendMessage', + }); + } + } + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, previewUrls); } catch (err: any) { SystemLogger.error({ msg: 'Error sending message:', err }); const errorMessage = typeof err === 'string' ? err : err.error || err.message; + const errorContext = err.details ?? {}; void api.broadcast('notify.ephemeralMessage', uid, message.rid, { - msg: i18n.t(errorMessage, { lng: user.language }), + msg: i18n.t(errorMessage, errorContext, user.language), }); if (typeof err === 'string') { diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 277841fd58e77..470fe0760b6d3 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -53,7 +53,7 @@ export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast { try { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx similarity index 100% rename from apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx rename to apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx diff --git a/apps/meteor/ee/app/livechat-enterprise/client/index.ts b/apps/meteor/app/livechat-enterprise/client/index.ts similarity index 90% rename from apps/meteor/ee/app/livechat-enterprise/client/index.ts rename to apps/meteor/app/livechat-enterprise/client/index.ts index 1fc3ef7041392..7be8700392765 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/index.ts +++ b/apps/meteor/app/livechat-enterprise/client/index.ts @@ -1,5 +1,4 @@ import { hasLicense } from '../../license/client'; -import '../lib/messageTypes'; import './startup'; void hasLicense('livechat-enterprise').then((enabled) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts similarity index 67% rename from apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts rename to apps/meteor/app/livechat-enterprise/client/messageTypes.ts index 9c15c277a1b27..90d390fe6be7b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts +++ b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts @@ -1,7 +1,26 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { MessageTypes } from '../../../../app/ui-utils/client'; -import { t } from '../../../../app/utils/lib/i18n'; +import { MessageTypes } from '../../ui-utils/client'; +import { t } from '../../utils/lib/i18n'; + +MessageTypes.registerType({ + id: 'livechat_transfer_history_fallback', + system: true, + message: 'New_chat_transfer_fallback', + data(message: any) { + if (!message.transferData) { + return { + fallback: 'SHOULD_NEVER_HAPPEN', + }; + } + const from = message.transferData.prevDepartment; + const to = message.transferData.department.name; + + return { + fallback: t('Livechat_transfer_failed_fallback', { from, to }), + }; + }, +}); MessageTypes.registerType({ id: 'omnichannel_priority_change_history', diff --git a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts b/apps/meteor/app/livechat-enterprise/client/startup.ts similarity index 60% rename from apps/meteor/ee/app/livechat-enterprise/client/startup.ts rename to apps/meteor/app/livechat-enterprise/client/startup.ts index 3c3ec1c02139c..0535f8926a7d4 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts +++ b/apps/meteor/app/livechat-enterprise/client/startup.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours'; -import type { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; -import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; -import { settings } from '../../../../app/settings/client'; import { hasLicense } from '../../license/client'; +import { businessHourManager } from '../../livechat/client/views/app/business-hours/BusinessHours'; +import type { IBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/Single'; +import { settings } from '../../settings/client'; import { MultipleBusinessHoursBehavior } from './views/business-hours/Multiple'; const businessHours: Record = { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts similarity index 79% rename from apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts rename to apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts index a57344da73dc1..698462dcaed2a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts @@ -1,7 +1,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { IBusinessHourBehavior } from '../../../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import type { IBusinessHourBehavior } from '../../../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; export class MultipleBusinessHoursBehavior implements IBusinessHourBehavior { getView(): string { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts similarity index 91% rename from apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts rename to apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts index c89931208451c..6d9d9f31e24c7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts @@ -1,5 +1,5 @@ -import { hasPermission, hasAtLeastOnePermission } from '../../../../../app/authorization/client'; -import { registerOmnichannelSidebarItem } from '../../../../../client/views/omnichannel/sidebarItems'; +import { registerOmnichannelSidebarItem } from '../../../../client/views/omnichannel/sidebarItems'; +import { hasPermission, hasAtLeastOnePermission } from '../../../authorization/client'; registerOmnichannelSidebarItem({ href: '/omnichannel/reports', diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 02239bc0edc51..48863fc9e5d31 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -4,6 +4,7 @@ import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings'; import { isTruthy } from '../../../../../lib/isTruthy'; import { API } from '../../../../api/server'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { findAppearance } from '../../../server/api/lib/appearance'; API.v1.addRoute( @@ -89,11 +90,13 @@ API.v1.addRoute( }) .toArray(); - await Promise.all( - dbSettings.filter(isTruthy).map((setting) => { - return Settings.updateValueById(setting._id, setting.value); - }), - ); + const eligibleSettings = dbSettings.filter(isTruthy); + const promises = eligibleSettings.map(({ _id, value }) => Settings.updateValueById(_id, value)); + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(eligibleSettings[index]._id); + } + }); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/server/api/v1/integration.ts b/apps/meteor/app/livechat/server/api/v1/integration.ts index 6cf8ffd52192f..a1f9c59ffb87f 100644 --- a/apps/meteor/app/livechat/server/api/v1/integration.ts +++ b/apps/meteor/app/livechat/server/api/v1/integration.ts @@ -3,6 +3,7 @@ import { isPOSTomnichannelIntegrations } from '@rocket.chat/rest-typings'; import { trim } from '../../../../../lib/utils/stringUtils'; import { API } from '../../../../api/server'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; API.v1.addRoute( 'omnichannel/integrations', @@ -23,53 +24,40 @@ API.v1.addRoute( LivechatWebhookOnAgentMessage, } = this.bodyParams; - const promises = []; + const settingsIds = [ + typeof LivechatWebhookUrl !== 'undefined' && { _id: 'Livechat_webhookUrl', value: trim(LivechatWebhookUrl) }, + typeof LivechatSecretToken !== 'undefined' && { _id: 'Livechat_secret_token', value: trim(LivechatSecretToken) }, + typeof LivechatHttpTimeout !== 'undefined' && { _id: 'Livechat_http_timeout', value: LivechatHttpTimeout }, + typeof LivechatWebhookOnStart !== 'undefined' && { _id: 'Livechat_webhook_on_start', value: !!LivechatWebhookOnStart }, + typeof LivechatWebhookOnClose !== 'undefined' && { _id: 'Livechat_webhook_on_close', value: !!LivechatWebhookOnClose }, + typeof LivechatWebhookOnChatTaken !== 'undefined' && { _id: 'Livechat_webhook_on_chat_taken', value: !!LivechatWebhookOnChatTaken }, + typeof LivechatWebhookOnChatQueued !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_queued', + value: !!LivechatWebhookOnChatQueued, + }, + typeof LivechatWebhookOnForward !== 'undefined' && { _id: 'Livechat_webhook_on_forward', value: !!LivechatWebhookOnForward }, + typeof LivechatWebhookOnOfflineMsg !== 'undefined' && { + _id: 'Livechat_webhook_on_offline_msg', + value: !!LivechatWebhookOnOfflineMsg, + }, + typeof LivechatWebhookOnVisitorMessage !== 'undefined' && { + _id: 'Livechat_webhook_on_visitor_message', + value: !!LivechatWebhookOnVisitorMessage, + }, + typeof LivechatWebhookOnAgentMessage !== 'undefined' && { + _id: 'Livechat_webhook_on_agent_message', + value: !!LivechatWebhookOnAgentMessage, + }, + ].filter(Boolean) as unknown as { _id: string; value: any }[]; - if (typeof LivechatWebhookUrl !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhookUrl', trim(LivechatWebhookUrl))); - } + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); - if (typeof LivechatSecretToken !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_secret_token', trim(LivechatSecretToken))); - } + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); - if (typeof LivechatHttpTimeout !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_http_timeout', LivechatHttpTimeout)); - } - - if (typeof LivechatWebhookOnStart !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_start', !!LivechatWebhookOnStart)); - } - - if (typeof LivechatWebhookOnClose !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_close', !!LivechatWebhookOnClose)); - } - - if (typeof LivechatWebhookOnChatTaken !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_chat_taken', !!LivechatWebhookOnChatTaken)); - } - - if (typeof LivechatWebhookOnChatQueued !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_chat_queued', !!LivechatWebhookOnChatQueued)); - } - - if (typeof LivechatWebhookOnForward !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_forward', !!LivechatWebhookOnForward)); - } - - if (typeof LivechatWebhookOnOfflineMsg !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_offline_msg', !!LivechatWebhookOnOfflineMsg)); - } - - if (typeof LivechatWebhookOnVisitorMessage !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_visitor_message', !!LivechatWebhookOnVisitorMessage)); - } - - if (typeof LivechatWebhookOnAgentMessage !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_agent_message', !!LivechatWebhookOnAgentMessage)); - } - - await Promise.all(promises); return API.v1.success(); }, }, diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 94df06ba418cb..dd9c701e64951 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,6 +6,7 @@ import { isGETWebRTCCall, isPUTWebRTCCallId } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; +import { notifyOnRoomChangedById, notifyOnSettingChanged } from '../../../../lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../../../settings/server'; import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; @@ -45,14 +46,20 @@ API.v1.addRoute( let { callStatus } = room; if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { - await Settings.incrementValueById('WebRTC_Calls_Count'); + const { value } = await Settings.incrementValueById('WebRTC_Calls_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + callStatus = 'ringing'; - await Rooms.setCallStatusAndCallStartTime(room._id, callStatus); + + (await Rooms.setCallStatusAndCallStartTime(room._id, callStatus)).modifiedCount && void notifyOnRoomChangedById(room._id); await Message.saveSystemMessage('livechat_webrtc_video_call', room._id, i18n.t('Join_my_room_to_start_the_video_call'), this.user, { actionLinks: config.theme.actionLinks.webrtc, }); } + const videoCall = { rid: room._id, provider: 'webrtc', diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index a5f11caaab63e..9ddd273600d50 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -5,6 +5,7 @@ import moment from 'moment-timezone'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -49,7 +50,7 @@ export abstract class AbstractBusinessHourBehavior { } async changeAgentActiveStatus(agentId: string, status: ILivechatAgentStatus): Promise { - return this.UsersRepository.setLivechatStatusIf( + const result = await this.UsersRepository.setLivechatStatusIf( agentId, status, // Why this works: statusDefault is the property set when a user manually changes their status @@ -57,6 +58,16 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true, statusDefault: { $ne: 'offline' } }, { livechatStatusSystemModified: true }, ); + + if (result.modifiedCount > 0) { + void notifyOnUserChange({ + clientAction: 'updated', + id: agentId, + diff: { statusLivechat: 'available', livechatStatusSystemModified: true }, + }); + } + + return result; } } diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index c893cb68ddf7c..ec21ff2de0677 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -5,6 +5,7 @@ import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/m import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; @@ -126,7 +127,12 @@ export class BusinessHourManager { return this.behavior.changeAgentActiveStatus(agentId, 'available'); } - return Users.setLivechatStatusActiveBasedOnBusinessHours(agentId); + const result = await Users.setLivechatStatusActiveBasedOnBusinessHours(agentId); + if (result.updatedCount > 0) { + void notifyOnUserChange({ clientAction: 'updated', id: agentId, diff: { statusLivechat: 'available ' } }); + } + + return result; } async restartCronJobsIfNecessary(): Promise { diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e50d866aa6b95..e19300691660a 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -1,8 +1,9 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import moment from 'moment'; +import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { businessHourLogger } from '../lib/logger'; import { createDefaultBusinessHourRow } from './LivechatBusinessHours'; import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; @@ -32,13 +33,14 @@ export const openBusinessHourDefault = async (): Promise => { active: 1, }, }); + const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id); businessHourLogger.debug({ msg: 'Opening default business hours', businessHoursToOpenIds }); await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds); if (businessHoursToOpenIds.length) { - await Users.makeAgentsWithinBusinessHourAvailable(); + await makeOnlineAgentsAvailable(); } - await Users.updateLivechatStatusBasedOnBusinessHours(); + await makeAgentsUnavailableBasedOnBusinessHour(); }; export const createDefaultBusinessHourIfNotExists = async (): Promise => { @@ -46,3 +48,55 @@ export const createDefaultBusinessHourIfNotExists = async (): Promise => { await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow()); } }; + +export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[] | null = null) { + const results = await Users.findAgentsAvailableWithoutBusinessHours(agentIds).toArray(); + + const update = await Users.updateLivechatStatusByAgentIds( + results.map(({ _id }) => _id), + ILivechatAgentStatus.NOT_AVAILABLE, + ); + + if (update.modifiedCount === 0) { + return; + } + + void notifyOnUserChangeAsync(async () => + results.map(({ _id, openBusinessHours }) => { + return { + id: _id, + clientAction: 'updated', + diff: { + statusLivechat: 'not-available', + openBusinessHours, + }, + }; + }), + ); +} + +export async function makeOnlineAgentsAvailable(agentIds: string[] | null = null) { + const results = await Users.findOnlineButNotAvailableAgents(agentIds).toArray(); + + const update = await Users.updateLivechatStatusByAgentIds( + results.map(({ _id }) => _id), + ILivechatAgentStatus.AVAILABLE, + ); + + if (update.modifiedCount === 0) { + return; + } + + void notifyOnUserChangeAsync(async () => + results.map(({ _id, openBusinessHours }) => { + return { + id: _id, + clientAction: 'updated', + diff: { + statusLivechat: 'available', + openBusinessHours, + }, + }; + }), + ); +} diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index 5d2730dba9a14..ea8166c75fa98 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,10 +1,11 @@ import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, makeAgentsUnavailableBasedOnBusinessHour, openBusinessHourDefault } from './Helper'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { @@ -18,7 +19,8 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp }) ).map((businessHour) => businessHour._id); await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds(businessHoursIds); - await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(); + + await makeAgentsUnavailableBasedOnBusinessHour(); } async onStartBusinessHours(): Promise { @@ -41,7 +43,19 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp agentId, newStatus: ILivechatAgentStatus.NOT_AVAILABLE, }); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + + const { modifiedCount } = await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + if (modifiedCount > 0) { + void notifyOnUserChange({ + id: agentId, + clientAction: 'updated', + diff: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + livechatStatusSystemModified: false, + }, + }); + } + return; } diff --git a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts index a2295b529272c..976d8ec1705ea 100644 --- a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts @@ -3,6 +3,7 @@ import { Users } from '@rocket.chat/models'; import { makeFunction } from '@rocket.chat/patch-injection'; import { businessHourLogger } from '../lib/logger'; +import { makeAgentsUnavailableBasedOnBusinessHour } from './Helper'; import { getAgentIdsForBusinessHour } from './getAgentIdsForBusinessHour'; export const closeBusinessHourByAgentIds = async ( @@ -16,7 +17,8 @@ export const closeBusinessHourByAgentIds = async ( top10AgentIds: agentIds.slice(0, 10), }); await Users.removeBusinessHourByAgentIds(agentIds, businessHourId); - await Users.updateLivechatStatusBasedOnBusinessHours(); + + await makeAgentsUnavailableBasedOnBusinessHour(); }; export const closeBusinessHour = makeFunction(async (businessHour: Pick): Promise => { diff --git a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts index 475eff9002fcc..5dcb9513ec2fc 100644 --- a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts +++ b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts @@ -1,13 +1,42 @@ import { LivechatDepartment, Users, LivechatDepartmentAgents, LivechatVisitors } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChanged, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; callbacks.add('livechat.afterAgentRemoved', async ({ agent }) => { - const departmentIds = (await LivechatDepartmentAgents.findByAgentId(agent._id).toArray()).map((department) => department.departmentId); - await Promise.all([ + const departments = await LivechatDepartmentAgents.findByAgentId(agent._id).toArray(); + + const [{ modifiedCount }, { deletedCount }] = await Promise.all([ Users.removeAgent(agent._id), LivechatDepartmentAgents.removeByAgentId(agent._id), agent.username && LivechatVisitors.removeContactManagerByUsername(agent.username), - departmentIds.length && LivechatDepartment.decreaseNumberOfAgentsByIds(departmentIds), + departments.length && LivechatDepartment.decreaseNumberOfAgentsByIds(departments.map(({ departmentId }) => departmentId)), ]); + + if (modifiedCount > 0) { + void notifyOnUserChange({ + id: agent._id, + clientAction: 'updated', + diff: { + operator: false, + livechat: null, + statusLivechat: null, + extension: null, + openBusinessHours: null, + }, + }); + } + + if (deletedCount > 0) { + departments.forEach((depAgent) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: depAgent._id, + agentId: agent._id, + departmentId: depAgent.departmentId, + }, + 'removed', + ); + }); + } }); diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 4a37dc4f0abeb..48ec985aa42cf 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -4,6 +4,7 @@ import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/m import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; callbacks.add( 'afterSaveMessage', @@ -37,10 +38,13 @@ callbacks.add( } if (!room.v?.activity?.includes(monthYear)) { - await Promise.all([ + const [, livechatInquiry] = await Promise.all([ LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear), LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear), ]); + if (livechatInquiry) { + void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v }); + } } if (room.responseBy) { diff --git a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts index 648337a8c2a10..e65f1d99b8842 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts @@ -2,6 +2,7 @@ import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatInquiry } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { RoutingManager } from '../lib/RoutingManager'; @@ -21,7 +22,10 @@ callbacks.add( return message; } - await LivechatInquiry.setLastMessageByRoomId(room._id, message); + const livechatInquiry = await LivechatInquiry.setLastMessageByRoomId(room._id, message); + if (livechatInquiry) { + void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { lastMessage: message }); + } return message; }, diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index ac304f38487c4..2e648b02f5dd4 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -6,6 +6,7 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; +import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../lib/server/lib/notifyListener'; import { i18n } from '../../../utils/lib/i18n'; type RegisterContactProps = { @@ -136,9 +137,15 @@ export const Contacts = { if (rooms?.length) { for await (const room of rooms) { const { _id: rid } = room; - (await Rooms.setFnameById(rid, name)) && - (await LivechatInquiry.setNameByRoomId(rid, name)) && - (await Subscriptions.updateDisplayNameByRoomId(rid, name)); + + await Promise.all([ + Rooms.setFnameById(rid, name), + LivechatInquiry.setNameByRoomId(rid, name), + Subscriptions.updateDisplayNameByRoomId(rid, name), + ]); + + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + void notifyOnRoomChangedById(rid); } } diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts index ed55a856e0b81..3dfa01e4f6b63 100644 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -1,8 +1,9 @@ -import type { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChanged } from '../../../lib/server/lib/notifyListener'; class DepartmentHelperClass { logger = new Logger('Omnichannel:DepartmentHelper'); @@ -24,29 +25,42 @@ class DepartmentHelperClass { throw new Error('error-failed-to-delete-department'); } - const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId>( - department._id, - { projection: { agentId: 1 } }, - ) - .cursor.map((agent) => agent.agentId) - .toArray(); + const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); this.logger.debug( `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, ); + const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); + const promiseResponses = await Promise.allSettled([ - LivechatDepartmentAgents.removeByDepartmentId(_id), + removeByDept, LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), ]); + promiseResponses.forEach((response, index) => { if (response.status === 'rejected') { this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); } }); - await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); + const { deletedCount } = await removeByDept; + + if (deletedCount > 0) { + removedAgents.forEach(({ _id: docId, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: docId, + agentId, + departmentId: _id, + }, + 'removed', + ); + }); + } + + await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); return ret; } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 453869d4425ac..dacd99be00f92 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -37,6 +37,10 @@ import { i18n } from '../../../../server/lib/i18n'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendNotification } from '../../../lib/server'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { + notifyOnLivechatDepartmentAgentChanged, + notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, +} from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; @@ -697,14 +701,31 @@ export const updateDepartmentAgents = async ( }); const { upsert = [], remove = [] } = agents; - const agentsRemoved = []; + + const agentsUpdated = []; + const agentsRemoved = remove.map(({ agentId }: { agentId: string }) => agentId); const agentsAdded = []; - for await (const { agentId } of remove) { - await LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(departmentId, agentId); - agentsRemoved.push(agentId); - } if (agentsRemoved.length > 0) { + const removedIds = await LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsRemoved, departmentId, { + projection: { agentId: 1 }, + }).toArray(); + + const { deletedCount } = await LivechatDepartmentAgents.removeByIds(removedIds.map(({ _id }) => _id)); + + if (deletedCount > 0) { + removedIds.forEach(({ _id, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id, + agentId, + departmentId, + }, + 'removed', + ); + }); + } + callbacks.runAsync('livechat.removeAgentDepartment', { departmentId, agentsId: agentsRemoved }); } @@ -714,7 +735,7 @@ export const updateDepartmentAgents = async ( continue; } - await LivechatDepartmentAgents.saveAgent({ + const livechatDepartmentAgent = await LivechatDepartmentAgents.saveAgent({ agentId: agent.agentId, departmentId, username: agentFromDb.username || '', @@ -722,6 +743,20 @@ export const updateDepartmentAgents = async ( order: agent.order ? parseFromIntOrStr(agent.order) : 0, departmentEnabled, }); + + if (livechatDepartmentAgent.upsertedId) { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: livechatDepartmentAgent.upsertedId as any, + agentId: agent.agentId, + departmentId, + }, + 'inserted', + ); + } else { + agentsUpdated.push(agent.agentId); + } + agentsAdded.push(agent.agentId); } @@ -732,6 +767,10 @@ export const updateDepartmentAgents = async ( }); } + if (agentsUpdated.length > 0) { + void notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId(agentsUpdated, departmentId); + } + if (agentsRemoved.length > 0 || agentsAdded.length > 0) { const numAgents = await LivechatDepartmentAgents.countByDepartmentId(departmentId); await LivechatDepartment.updateNumAgentsById(departmentId, numAgents); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 588e3cdac7236..bf5014b984f18 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -57,6 +57,14 @@ import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; +import { + notifyOnLivechatInquiryChanged, + notifyOnLivechatInquiryChangedByRoom, + notifyOnRoomChangedById, + notifyOnLivechatInquiryChangedByToken, + notifyOnLivechatDepartmentAgentChangedByDepartmentId, + notifyOnUserChange, +} from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -292,10 +300,15 @@ class LivechatClass { this.logger.debug(`Updating DB for room ${room._id} with close data`); + const inquiry = await LivechatInquiry.findOneByRoomId(rid); + const removedInquiry = await LivechatInquiry.removeByRoomId(rid); if (removedInquiry && removedInquiry.deletedCount !== 1) { throw new Error('Error removing inquiry'); } + if (inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData); if (!updatedRoom || updatedRoom.modifiedCount !== 1) { @@ -347,6 +360,8 @@ class LivechatClass { }); } + void notifyOnRoomChangedById(newRoom._id); + this.logger.debug(`Room ${newRoom._id} was closed`); } @@ -497,6 +512,8 @@ class LivechatClass { throw new Meteor.Error('error-invalid-room', 'Invalid room'); } + const inquiry = await LivechatInquiry.findOneByRoomId(rid); + const result = await Promise.allSettled([ Messages.removeByRoomId(rid), ReadReceipts.removeByRoomId(rid), @@ -505,6 +522,10 @@ class LivechatClass { LivechatRooms.removeById(rid), ]); + if (inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } + for (const r of result) { if (r.status === 'rejected') { this.logger.error(`Error removing room ${rid}: ${r.reason}`); @@ -999,6 +1020,8 @@ class LivechatClass { await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + await callbacks.run('livechat.afterDepartmentArchived', department); } @@ -1011,6 +1034,9 @@ class LivechatClass { // TODO: these kind of actions should be on events instead of here await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + return true; } @@ -1262,11 +1288,11 @@ class LivechatClass { ]); } - await Promise.all([ - Subscriptions.removeByVisitorToken(token), - LivechatRooms.removeByVisitorToken(token), - LivechatInquiry.removeByVisitorToken(token), - ]); + await Promise.all([Subscriptions.removeByVisitorToken(token), LivechatRooms.removeByVisitorToken(token)]); + + const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); + await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); + void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); } async deleteMessage({ guest, message }: { guest: ILivechatVisitor; message: IMessage }) { @@ -1283,9 +1309,18 @@ class LivechatClass { } async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { - const user = await Users.setLivechatStatusIf(userId, status, condition, fields); + const result = await Users.setLivechatStatusIf(userId, status, condition, fields); + + if (result.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { ...fields, statusLivechat: status }, + }); + } + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - return user; + return result; } async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { @@ -1655,13 +1690,30 @@ class LivechatClass { } async notifyGuestStatusChanged(token: string, status: UserStatus) { - await LivechatInquiry.updateVisitorStatus(token, status); await LivechatRooms.updateVisitorStatus(token, status); + + const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status); + + if (inquiryVisitorStatus.modifiedCount) { + void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } }); + } } async setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { const user = await Users.setLivechatStatus(userId, status); callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + + if (user.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { + statusLivechat: status, + livechatStatusSystemModified: false, + }, + }); + } + return user; } @@ -1801,14 +1853,19 @@ class LivechatClass { if (guestData?.name?.trim().length) { const { _id: rid } = roomData; const { name } = guestData; + await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); - return true; + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); } + + void notifyOnRoomChangedById(roomData._id); + + return true; } /** diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 8be71aa4c9917..576b29990b33a 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,20 +1,42 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; +import { + LivechatInquiryStatus, + type ILivechatInquiryRecord, + type ILivechatVisitor, + type IMessage, + type IOmnichannelRoom, + type SelectedAgent, +} from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { + notifyOnLivechatInquiryChangedById, + notifyOnLivechatInquiryChanged, + notifyOnSettingChanged, +} from '../../../lib/server/lib/notifyListener'; import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper'; import { RoutingManager } from './RoutingManager'; const logger = new Logger('QueueManager'); export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { - await LivechatInquiry.queueInquiry(inquiry._id); - await callbacks.run('livechat.afterInquiryQueued', inquiry); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + if (!queuedInquiry) { + return; + } + + await callbacks.run('livechat.afterInquiryQueued', queuedInquiry); + + void notifyOnLivechatInquiryChanged(queuedInquiry, 'updated', { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + takenAt: undefined, + }); }; export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => { @@ -106,7 +128,11 @@ export const QueueManager: queueManager = { } void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); - await LivechatRooms.updateRoomCount(); + + const livechatSetting = await LivechatRooms.updateRoomCount(); + if (livechatSetting) { + void notifyOnSettingChanged(livechatSetting); + } await queueInquiry(inquiry, agent); logger.debug(`Inquiry ${inquiry._id} queued`); @@ -137,6 +163,7 @@ export const QueueManager: queueManager = { if (oldInquiry) { logger.debug(`Removing old inquiry (${oldInquiry._id}) for room ${rid}`); await LivechatInquiry.removeByRoomId(rid); + void notifyOnLivechatInquiryChangedById(oldInquiry._id, 'removed'); } const guest = { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 19437d800ee21..5782d01e318fb 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -11,6 +11,7 @@ import type { InquiryWithAgentInfo, TransferData, } from '@rocket.chat/core-typings'; +import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; @@ -18,6 +19,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { createLivechatSubscription, @@ -182,7 +184,15 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (shouldQueue) { - await LivechatInquiry.queueInquiry(inquiry._id); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + if (queuedInquiry) { + inquiry = queuedInquiry; + void notifyOnLivechatInquiryChanged(inquiry, 'updated', { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + takenAt: undefined, + }); + } } if (servedBy) { @@ -192,6 +202,7 @@ export const RoutingManager: Routing = { } await dispatchInquiryQueued(inquiry); + return true; }, @@ -250,11 +261,20 @@ export const RoutingManager: Routing = { } await LivechatInquiry.takeInquiry(_id); + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); + void notifyOnLivechatInquiryChangedById(inquiry._id, 'updated', { + status: LivechatInquiryStatus.TAKEN, + takenAt: new Date(), + defaultAgent: undefined, + estimatedInactivityCloseTimeAt: undefined, + queuedAt: undefined, + }); + return LivechatRooms.findOneById(rid); }, @@ -282,6 +302,7 @@ export const RoutingManager: Routing = { if (defaultAgent) { logger.debug(`Delegating Inquiry ${inquiry._id} to agent ${defaultAgent.username}`); await LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent); + void notifyOnLivechatInquiryChanged(inquiry, 'updated', { defaultAgent }); } logger.debug(`Queueing inquiry ${inquiry._id}`); diff --git a/apps/meteor/app/livechat/server/methods/saveAppearance.ts b/apps/meteor/app/livechat/server/methods/saveAppearance.ts index 35152d136afdc..cac5d34264d20 100644 --- a/apps/meteor/app/livechat/server/methods/saveAppearance.ts +++ b/apps/meteor/app/livechat/server/methods/saveAppearance.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveAppearance'(settings) { methodDeprecationLogger.method('livechat:saveAppearance', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -50,10 +52,12 @@ Meteor.methods({ throw new Meteor.Error('invalid-setting'); } - await Promise.all( - settings.map((setting) => { - return Settings.updateValueById(setting._id, setting.value); - }), - ); + const promises = settings.map((setting) => Settings.updateValueById(setting._id, setting.value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settings[index]._id); + } + }); }, }); diff --git a/apps/meteor/app/livechat/server/methods/saveIntegration.ts b/apps/meteor/app/livechat/server/methods/saveIntegration.ts index 18bad34f0aea6..de7461d08e100 100644 --- a/apps/meteor/app/livechat/server/methods/saveIntegration.ts +++ b/apps/meteor/app/livechat/server/methods/saveIntegration.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,56 +16,59 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveIntegration'(values) { + methodDeprecationLogger.method('livechat:saveIntegration', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveIntegration', }); } - methodDeprecationLogger.method('livechat:saveIntegration', '7.0.0'); - - if (typeof values.Livechat_webhookUrl !== 'undefined') { - await Settings.updateValueById('Livechat_webhookUrl', trim(values.Livechat_webhookUrl)); - } - - if (typeof values.Livechat_secret_token !== 'undefined') { - await Settings.updateValueById('Livechat_secret_token', trim(values.Livechat_secret_token)); - } - - if (typeof values.Livechat_http_timeout === 'number') { - await Settings.updateValueById('Livechat_http_timeout', values.Livechat_http_timeout); - } - - if (typeof values.Livechat_webhook_on_start !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); - } - if (typeof values.Livechat_webhook_on_close !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); - } - - if (typeof values.Livechat_webhook_on_chat_taken !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); - } + const settingsIds = [ + typeof values.Livechat_webhookUrl !== 'undefined' && { _id: 'Livechat_webhookUrl', value: trim(values.Livechat_webhookUrl) }, + typeof values.Livechat_secret_token !== 'undefined' && { _id: 'Livechat_secret_token', value: trim(values.Livechat_secret_token) }, + typeof values.Livechat_http_timeout !== 'undefined' && { _id: 'Livechat_http_timeout', value: values.Livechat_http_timeout }, + typeof values.Livechat_webhook_on_start !== 'undefined' && { + _id: 'Livechat_webhook_on_start', + value: !!values.Livechat_webhook_on_start, + }, + typeof values.Livechat_webhook_on_close !== 'undefined' && { + _id: 'Livechat_webhook_on_close', + value: !!values.Livechat_webhook_on_close, + }, + typeof values.Livechat_webhook_on_forward !== 'undefined' && { + _id: 'Livechat_webhook_on_forward', + value: !!values.Livechat_webhook_on_forward, + }, + typeof values.Livechat_webhook_on_chat_taken !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_taken', + value: !!values.Livechat_webhook_on_chat_taken, + }, + typeof values.Livechat_webhook_on_chat_queued !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_queued', + value: !!values.Livechat_webhook_on_chat_queued, + }, + typeof values.Livechat_webhook_on_offline_msg !== 'undefined' && { + _id: 'Livechat_webhook_on_offline_msg', + value: !!values.Livechat_webhook_on_offline_msg, + }, + typeof values.Livechat_webhook_on_visitor_message !== 'undefined' && { + _id: 'Livechat_webhook_on_visitor_message', + value: !!values.Livechat_webhook_on_visitor_message, + }, + typeof values.Livechat_webhook_on_agent_message !== 'undefined' && { + _id: 'Livechat_webhook_on_agent_message', + value: !!values.Livechat_webhook_on_agent_message, + }, + ].filter(Boolean) as unknown as { _id: string; value: any }[]; - if (typeof values.Livechat_webhook_on_chat_queued !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); - } - - if (typeof values.Livechat_webhook_on_forward !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); - } + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); - if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); - } - - if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); - } - - if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); - } + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); }, }); diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index aba8578dcea98..b61c85c4001ea 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -10,6 +10,7 @@ import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomC import { i18n } from '../../../server/lib/i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; @@ -64,29 +65,47 @@ Meteor.startup(async () => { }); await createDefaultBusinessHourIfNotExists(); - settings.watch('Livechat_enable_business_hours', async (value) => { - logger.debug(`Starting business hour manager ${value}`); - if (value) { - await businessHourManager.startManager(); - return; - } - await businessHourManager.stopManager(); - }); + settings.watch( + 'Livechat_enable_business_hours', + async (value) => { + logger.debug(`Starting business hour manager ${value}`); + if (value) { + await businessHourManager.startManager(); + return; + } + await businessHourManager.stopManager(); + }, + process.env.TEST_MODE === 'true' + ? { + debounce: 10, + } + : undefined, + ); settings.watch('Livechat_Routing_Method', () => { void RoutingManager.startQueue(); }); // Remove when accounts.onLogout is async - Accounts.onLogout( - ({ user }: { user: IUser }) => - user?.roles?.includes('livechat-agent') && - !user?.roles?.includes('bot') && - void LivechatTyped.setUserStatusLivechatIf( - user._id, - ILivechatAgentStatus.NOT_AVAILABLE, - {}, - { livechatStatusSystemModified: true }, - ).catch(), - ); + Accounts.onLogout(({ user }: { user: IUser }) => { + if (!user?.roles?.includes('livechat-agent') || user?.roles?.includes('bot')) { + return; + } + + void LivechatTyped.setUserStatusLivechatIf( + user._id, + ILivechatAgentStatus.NOT_AVAILABLE, + {}, + { livechatStatusSystemModified: true }, + ).catch(); + + void notifyOnUserChange({ + id: user._id, + clientAction: 'updated', + diff: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + livechatStatusSystemModified: true, + }, + }); + }); }); diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index e562fc8e7b39f..7e0ad0380b119 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -11,6 +11,7 @@ import _ from 'underscore'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { replaceVariables } from './replaceVariables'; @@ -166,7 +167,10 @@ export const sendNoWrap = async ({ html = undefined; } - await Settings.incrementValueById('Triggered_Emails_Count'); + const { value } = await Settings.incrementValueById('Triggered_Emails_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } const email = { to, from, replyTo, subject, html, text, headers }; diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index dc17a75a0192c..b0eab3f929d66 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -12,6 +12,7 @@ import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/m import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; +import { notifyOnRoomChangedById } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; @@ -116,6 +117,7 @@ Meteor.methods({ } if (isTheLastMessage(room, message)) { await Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); + void notifyOnRoomChangedById(room._id); } const attachments: MessageAttachment[] = []; @@ -131,7 +133,9 @@ Meteor.methods({ // App IPostMessagePinned event hook await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); - const msgId = await Message.saveSystemMessage('message_pinned', originalMessage.rid, '', me, { + const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned'; + + const msgId = await Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { attachments: [ { text: originalMessage.msg, @@ -213,6 +217,7 @@ Meteor.methods({ if (isTheLastMessage(room, message)) { await Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); + void notifyOnRoomChangedById(room._id); } // App IPostMessagePinned event hook diff --git a/apps/meteor/app/message-star/server/starMessage.ts b/apps/meteor/app/message-star/server/starMessage.ts index 7ac8fd619d31a..4529efb63f6fb 100644 --- a/apps/meteor/app/message-star/server/starMessage.ts +++ b/apps/meteor/app/message-star/server/starMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; +import { notifyOnRoomChangedById } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; declare module '@rocket.chat/ui-contexts' { @@ -55,6 +56,7 @@ Meteor.methods({ if (isTheLastMessage(room, message)) { await Rooms.updateLastMessageStar(room._id, uid, message.starred); + void notifyOnRoomChangedById(room._id); } await Apps.self?.triggerEvent(AppEvents.IPostMessageStarred, message, await Meteor.userAsync(), message.starred); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 8f6791c36302d..bb95672603377 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -3,6 +3,10 @@ import { LoginServiceConfiguration } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { + notifyOnLoginServiceConfigurationChanged, + notifyOnLoginServiceConfigurationChangedByService, +} from '../../../lib/server/lib/notifyListener'; import { settings, settingsRegistry } from '../../../settings/server'; import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { SAMLUtils } from './Utils'; @@ -117,9 +121,22 @@ export const loadSamlServiceProviders = async function (): Promise { const samlConfigs = getSamlConfigs(key); SAMLUtils.log(key); await LoginServiceConfiguration.createOrUpdateService(serviceName, samlConfigs); + void notifyOnLoginServiceConfigurationChangedByService(serviceName); return configureSamlService(samlConfigs); } - await LoginServiceConfiguration.removeService(serviceName); + + const service = await LoginServiceConfiguration.findOneByService(serviceName, { projection: { _id: 1 } }); + if (!service?._id) { + return false; + } + + const { deletedCount } = await LoginServiceConfiguration.removeService(service._id); + if (!deletedCount) { + return false; + } + + void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed'); + return false; }), ) diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 136686f49c9e3..978b3d59ec98d 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -39,7 +39,7 @@ const setPrometheusData = async (): Promise => { metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length); // Apps metrics - const { totalInstalled, totalActive, totalFailed } = getAppsStatistics(); + const { totalInstalled, totalActive, totalFailed } = await getAppsStatistics(); metrics.totalAppsInstalled.set(totalInstalled || 0); metrics.totalAppsEnabled.set(totalActive || 0); diff --git a/apps/meteor/app/otr/client/OTR.ts b/apps/meteor/app/otr/client/OTR.ts index a855381bd9c05..9f3eea155384a 100644 --- a/apps/meteor/app/otr/client/OTR.ts +++ b/apps/meteor/app/otr/client/OTR.ts @@ -24,6 +24,16 @@ class OTR implements IOTR { this.instancesByRoomId[rid] = otrRoom; return this.instancesByRoomId[rid]; } + + closeAllInstances(): void { + // Resets state, but doesnt emit events + // Other party should receive event and fire events + Object.values(this.instancesByRoomId).forEach((instance) => { + instance.softReset(); + }); + + this.instancesByRoomId = {}; + } } export default new OTR(); diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 8899de13b1908..ea5dc86b83195 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -1,4 +1,5 @@ import type { IRoom, IMessage, IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; @@ -7,6 +8,7 @@ import { Tracker } from 'meteor/tracker'; import GenericModal from '../../../client/components/GenericModal'; import { imperativeModal } from '../../../client/lib/imperativeModal'; +import type { UserPresence } from '../../../client/lib/presence'; import { Presence } from '../../../client/lib/presence'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; @@ -47,6 +49,8 @@ export class OTRRoom implements IOTRRoom { private isFirstOTR: boolean; + private onPresenceEventHook: (event: UserPresence | undefined) => void; + protected constructor(uid: IUser['_id'], rid: IRoom['_id'], peerId: IUser['_id']) { this._userId = uid; this._roomId = rid; @@ -54,6 +58,7 @@ export class OTRRoom implements IOTRRoom { this._sessionKey = null; this.peerId = peerId; this.isFirstOTR = true; + this.onPresenceEventHook = this.onPresenceEvent.bind(this); } public static create(uid: IUser['_id'], rid: IRoom['_id']): OTRRoom | undefined { @@ -110,6 +115,35 @@ export class OTRRoom implements IOTRRoom { } } + onPresenceEvent(event: UserPresence | undefined): void { + if (!event) { + return; + } + if (event.status !== UserStatus.OFFLINE) { + return; + } + console.warn(`OTR Room ${this._roomId} ended because ${this.peerId} went offline`); + this.end(); + + imperativeModal.open({ + component: GenericModal, + props: { + variant: 'warning', + title: t('OTR'), + children: t('OTR_Session_ended_other_user_went_offline', { username: event.username }), + confirmText: t('Ok'), + onClose: imperativeModal.close, + onConfirm: imperativeModal.close, + }, + }); + } + + // Starts listening to other user's status changes and end OTR if any of the Users goes offline + // this should be called in 2 places: on acknowledge (meaning user accepted OTR) or on establish (meaning user initiated OTR) + listenToUserStatus(): void { + Presence.listen(this.peerId, this.onPresenceEventHook); + } + acknowledge(): void { void sdk.rest.post('/v1/statistics.telemetry', { params: [{ eventName: 'otrStats', timestamp: Date.now(), rid: this._roomId }] }); @@ -137,10 +171,19 @@ export class OTRRoom implements IOTRRoom { ]); } + softReset(): void { + this.isFirstOTR = true; + this.setState(OtrRoomState.NOT_STARTED); + this._keyPair = null; + this._exportedPublicKey = {}; + this._sessionKey = null; + } + end(): void { this.isFirstOTR = true; this.reset(); this.setState(OtrRoomState.NOT_STARTED); + Presence.stop(this.peerId, this.onPresenceEventHook); sdk.publish('notify-user', [ `${this.peerId}/otr`, 'end', @@ -285,6 +328,7 @@ export class OTRRoom implements IOTRRoom { setTimeout(async () => { this.setState(OtrRoomState.ESTABLISHED); this.acknowledge(); + this.listenToUserStatus(); if (data.refresh) { await sdk.rest.post('/v1/chat.otr', { @@ -362,6 +406,7 @@ export class OTRRoom implements IOTRRoom { this.setState(OtrRoomState.ESTABLISHED); if (this.isFirstOTR) { + this.listenToUserStatus(); await sdk.rest.post('/v1/chat.otr', { roomId: this._roomId, type: otrSystemMessages.USER_JOINED_OTR, diff --git a/apps/meteor/app/otr/client/events.ts b/apps/meteor/app/otr/client/events.ts new file mode 100644 index 0000000000000..9ff84c4651570 --- /dev/null +++ b/apps/meteor/app/otr/client/events.ts @@ -0,0 +1,7 @@ +import { Accounts } from 'meteor/accounts-base'; + +import OTR from './OTR'; + +Accounts.onLogout(() => { + OTR.closeAllInstances(); +}); diff --git a/apps/meteor/app/otr/client/index.ts b/apps/meteor/app/otr/client/index.ts index 74fea3c003e8e..fac7407f54fac 100644 --- a/apps/meteor/app/otr/client/index.ts +++ b/apps/meteor/app/otr/client/index.ts @@ -1,3 +1,4 @@ import './OTRRoom'; import './OTR'; import './messageTypes'; +import './events'; diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts index 819e26e4f0034..87ced6e130df0 100644 --- a/apps/meteor/app/push/server/fcm.ts +++ b/apps/meteor/app/push/server/fcm.ts @@ -55,15 +55,17 @@ type FCMError = { }; /** - * Set at least a 10 second timeout on send requests before retrying. - * Most of FCM's internal Remote Procedure Calls use a 10 second timeout. + * Send a push notification using Firebase Cloud Messaging (FCM). + * implements the Firebase Cloud Messaging HTTP v1 API, and all of its retry logic, + * see: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode * * Errors: - * - For 400, 401, 403, 404 errors: abort, and do not retry. + * - For 400, 401, 403 errors: abort, and do not retry. + * - For 404 errors: remove the token from the database. * - For 429 errors: retry after waiting for the duration set in the retry-after header. If no retry-after header is set, default to 60 seconds. * - For 500 errors: retry with exponential backoff. */ -async function fetchWithRetry(url: string, options: RequestInit, retries = 0): Promise { +async function fetchWithRetry(url: string, _removeToken: () => void, options: RequestInit, retries = 0): Promise { const MAX_RETRIES = 5; const response = await fetch(url, options); @@ -79,15 +81,20 @@ async function fetchWithRetry(url: string, options: RequestInit, retries = 0): P const retryAfter = response.headers.get('retry-after'); const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; + if (response.status === 404) { + _removeToken(); + return response; + } + if (response.status === 429) { await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } if (response.status >= 500 && response.status < 600) { const backoff = Math.pow(2, retries) * 10000; await new Promise((resolve) => setTimeout(resolve, backoff)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } const error: FCMError = await response.json(); @@ -145,12 +152,7 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP return userTokens.map((token) => ({ message: { ...message, token } })); } -export const sendFCM = function ({ userTokens, notification, _replaceToken, _removeToken, options }: NativeNotificationParameters): void { - // We don't use these parameters, but we need to keep them to keep the function signature - // TODO: Remove them when we remove the old sendGCM function - _replaceToken; - _removeToken; - +export const sendFCM = function ({ userTokens, notification, _removeToken, options }: NativeNotificationParameters): void { const tokens = typeof userTokens === 'string' ? [userTokens] : userTokens; if (!tokens.length) { logger.log('sendFCM no push tokens found'); @@ -173,9 +175,15 @@ export const sendFCM = function ({ userTokens, notification, _replaceToken, _rem const url = `https://fcm.googleapis.com/v1/projects/${options.gcm.projectNumber}/messages:send`; - for (const message of messages) { - logger.debug('sendFCM message', message); - const response = fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify(message) }); + for (const fcmRequest of messages) { + logger.debug('sendFCM message', fcmRequest); + + const removeToken = () => { + const { token } = fcmRequest.message; + token && _removeToken({ gcm: token }); + }; + + const response = fetchWithRetry(url, removeToken, { method: 'POST', headers, body: JSON.stringify(fcmRequest) }); response.catch((err) => { logger.error('sendFCM error', err); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 36eaab6955123..896e5041bd615 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -13,6 +13,7 @@ import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { emoji } from '../../emoji/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; +import { notifyOnRoomChangedById } from '../../lib/server/lib/notifyListener'; const removeUserReaction = (message: IMessage, reaction: string, username: string) => { if (!message.reactions) { @@ -74,6 +75,7 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction delete message.reactions; if (isTheLastMessage(room, message)) { await Rooms.unsetReactionsInLastMessage(room._id); + void notifyOnRoomChangedById(room._id); } await Messages.unsetReactions(message._id); } else { @@ -99,6 +101,7 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); + void notifyOnRoomChangedById(room._id); } await callbacks.run('setReaction', message._id, reaction); await callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts index fb0e691abd69f..337691bfbe57a 100644 --- a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts +++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts @@ -2,6 +2,7 @@ import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Rooms } from '@rocket.chat/models'; +import { getCronAdvancedTimerFromPrecisionSetting } from '../../../lib/getCronAdvancedTimerFromPrecisionSetting'; import { cleanRoomHistory } from '../../lib/server/functions/cleanRoomHistory'; import { settings } from '../../settings/server'; @@ -29,7 +30,7 @@ async function job(): Promise { // get all rooms with default values for await (const type of types) { const maxAge = maxTimes[type] || 0; - const latest = new Date(now.getTime() - toDays(maxAge)); + const latest = new Date(now.getTime() - maxAge); const rooms = await Rooms.find( { @@ -79,19 +80,6 @@ async function job(): Promise { } } -function getSchedule(precision: '0' | '1' | '2' | '3'): string { - switch (precision) { - case '0': - return '*/30 * * * *'; // 30 minutes - case '1': - return '0 * * * *'; // hour - case '2': - return '0 */6 * * *'; // 6 hours - case '3': - return '0 0 * * *'; // day - } -} - const pruneCronName = 'Prune old messages by retention policy'; async function deployCron(precision: string): Promise { @@ -107,9 +95,9 @@ settings.watchMultiple( 'RetentionPolicy_AppliesToChannels', 'RetentionPolicy_AppliesToGroups', 'RetentionPolicy_AppliesToDMs', - 'RetentionPolicy_MaxAge_Channels', - 'RetentionPolicy_MaxAge_Groups', - 'RetentionPolicy_MaxAge_DMs', + 'RetentionPolicy_TTL_Channels', + 'RetentionPolicy_TTL_Groups', + 'RetentionPolicy_TTL_DMs', 'RetentionPolicy_Advanced_Precision', 'RetentionPolicy_Advanced_Precision_Cron', 'RetentionPolicy_Precision', @@ -132,13 +120,13 @@ settings.watchMultiple( types.push('d'); } - maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels'); - maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups'); - maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs'); + maxTimes.c = settings.get('RetentionPolicy_TTL_Channels'); + maxTimes.p = settings.get('RetentionPolicy_TTL_Groups'); + maxTimes.d = settings.get('RetentionPolicy_TTL_DMs'); const precision = (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || - getSchedule(settings.get('RetentionPolicy_Precision')); + getCronAdvancedTimerFromPrecisionSetting(settings.get('RetentionPolicy_Precision')); return deployCron(precision); }, diff --git a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts index c9eb356841be6..af88e3c548adb 100644 --- a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts +++ b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts @@ -1,11 +1,17 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import telemetryEvent from '../lib/telemetryEvents'; type updateCounterDataType = { settingsId: string }; export function updateCounter(data: updateCounterDataType): void { - void Settings.incrementValueById(data.settingsId); + void (async () => { + const { value } = await Settings.incrementValueById(data.settingsId, 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + })(); } telemetryEvent.register('updateCounter', updateCounter); diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js deleted file mode 100644 index 1d84bead3e85c..0000000000000 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Apps } from '@rocket.chat/apps'; -import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; - -import { Info } from '../../../utils/rocketchat.info'; - -export function getAppsStatistics() { - return { - engineVersion: Info.marketplaceApiVersion, - totalInstalled: (Apps.self?.isInitialized() && Apps.getManager().get().length) ?? 0, - totalActive: (Apps.self?.isInitialized() && Apps.getManager().get({ enabled: true }).length) ?? 0, - totalFailed: - (Apps.self?.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length) ?? - 0, - }; -} diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts new file mode 100644 index 0000000000000..930fc15a9c555 --- /dev/null +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -0,0 +1,51 @@ +import { Apps } from '@rocket.chat/apps'; +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import { Info } from '../../../utils/rocketchat.info'; + +export type AppsStatistics = { + engineVersion: string; + totalInstalled: number | false; + totalActive: number | false; + totalFailed: number | false; +}; + +export async function getAppsStatistics(): Promise { + if (!Apps.self?.isInitialized()) { + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled: false, + totalActive: false, + totalFailed: false, + }; + } + + const apps = await Apps.getManager().get(); + + let totalInstalled = 0; + let totalActive = 0; + let totalFailed = 0; + + await Promise.all( + apps.map(async (app) => { + totalInstalled++; + + const status = await app.getStatus(); + + if (status === AppStatus.MANUALLY_DISABLED) { + totalFailed++; + } + + if (AppStatusUtils.isEnabled(status)) { + totalActive++; + } + }), + ); + + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled, + totalActive, + totalFailed, + }; +} diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index a6fcc5b17b5b1..cff2aaefcc5ac 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -467,7 +467,7 @@ export const statistics = { }), ); - statistics.apps = getAppsStatistics(); + statistics.apps = await getAppsStatistics(); statistics.services = await getServicesStatistics(); statistics.importer = getImporterStatistics(); statistics.videoConf = await VideoConf.getStatistics(); diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index 77190992612a8..545e1e73342d4 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -356,12 +356,6 @@ export class CachedCollection extends Emitter< this.trySync(); }); - if (!this.userRelated) { - return this.setupListener(); - } - - CachedCollectionManager.onLogin(async () => { - await this.setupListener(); - }); + return this.setupListener(); } } diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 6a3ddd45ca66d..c1f9590b98ee8 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -25,7 +25,7 @@ export type MessageActionContext = type MessageActionType = 'communication' | 'interaction' | 'duplication' | 'apps' | 'management'; -type MessageActionConditionProps = { +export type MessageActionConditionProps = { message: IMessage; user: IUser | undefined; room: IRoom; @@ -65,6 +65,7 @@ export type MessageActionConfig = { ) => any; condition?: (props: MessageActionConditionProps) => Promise | boolean; type?: MessageActionType; + disabled?: (props: MessageActionConditionProps) => boolean; }; class MessageAction { diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index f3cf0be67ca99..2f2793f7493b9 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; +import { isE2EEMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -63,6 +63,9 @@ Meteor.startup(async () => { }, order: 0, group: 'menu', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -87,6 +90,9 @@ Meteor.startup(async () => { }, order: 0, group: 'message', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -139,6 +145,9 @@ Meteor.startup(async () => { }, order: 5, group: 'menu', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -185,7 +194,7 @@ Meteor.startup(async () => { return false; } const blockEditInMinutes = settings.Message_AllowEditing_BlockEditInMinutes as number; - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); if (!bypassBlockTimeLimit && blockEditInMinutes) { let msgTs; diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 18ff309970dfc..c174f9125f49f 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -51,6 +51,7 @@ type EventMap = Str type StreamMapValue = { stop: () => void; + error: (cb: (...args: any[]) => void) => void; onChange: ReturnType['onChange']; ready: () => Promise; isReady: boolean; @@ -62,6 +63,7 @@ const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys { - console.error(err); ee.emit('ready', [err]); + ee.emit('error', err); }, }, ); @@ -115,6 +117,11 @@ const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys void) => + ee.once('error', (error) => { + cb(error); + }), + get isReady() { return meta.ready; }, @@ -179,6 +186,7 @@ const createStreamManager = () => { if (!streams.has(eventLiteral)) { streams.set(eventLiteral, stream); } + stream.error(() => stop()); return { id: '', diff --git a/apps/meteor/app/utils/client/restrictions.ts b/apps/meteor/app/utils/client/restrictions.ts index 261eddf4467dc..d4c6b62d68dd9 100644 --- a/apps/meteor/app/utils/client/restrictions.ts +++ b/apps/meteor/app/utils/client/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/client'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/app/utils/lib/mimeTypes.ts b/apps/meteor/app/utils/lib/mimeTypes.ts index f2da185f84ba8..909a955d6724d 100644 --- a/apps/meteor/app/utils/lib/mimeTypes.ts +++ b/apps/meteor/app/utils/lib/mimeTypes.ts @@ -12,4 +12,9 @@ const getExtension = (param: string): string => { return !extension || typeof extension === 'boolean' ? '' : extension; }; -export { mime, getExtension }; +const getMimeType = (fileName: string): string => { + const fileMimeType = mime.lookup(fileName); + return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream'; +}; + +export { mime, getExtension, getMimeType }; diff --git a/apps/meteor/app/utils/lib/restrictions.ts b/apps/meteor/app/utils/lib/restrictions.ts index ebebe113f31e6..bf859e5b4700c 100644 --- a/apps/meteor/app/utils/lib/restrictions.ts +++ b/apps/meteor/app/utils/lib/restrictions.ts @@ -1,12 +1,10 @@ -import _ from 'underscore'; - export const fileUploadMediaWhiteList = function (customWhiteList: string): string[] | undefined { const mediaTypeWhiteList = customWhiteList; if (!mediaTypeWhiteList || mediaTypeWhiteList === '*') { return; } - return _.map(mediaTypeWhiteList.split(','), (item) => { + return mediaTypeWhiteList.split(',').map((item) => { return item.trim(); }); }; @@ -17,37 +15,47 @@ const fileUploadMediaBlackList = function (customBlackList: string): string[] | return; } - return _.map(blacklist.split(','), (item) => item.trim()); + return blacklist.split(',').map((item) => item.trim()); }; -const isTypeOnList = function (type: string, list: string[]): boolean | undefined { - if (_.contains(list, type)) { +const isTypeOnList = function (type?: string, list?: string[]): boolean { + if (!type || !list) { + return false; + } + + if (list.includes(type)) { return true; } const wildCardGlob = '/*'; - const wildcards = _.filter(list, (item) => { + const wildcards = list.filter((item) => { return item.indexOf(wildCardGlob) > 0; }); - if (_.contains(wildcards, type.replace(/(\/.*)$/, wildCardGlob))) { + if (wildcards.includes(type.replace(/(\/.*)$/, wildCardGlob))) { return true; } + + return false; }; -export const fileUploadIsValidContentTypeFromSettings = function (type: string, customWhiteList: string, customBlackList: string): boolean { +export const fileUploadIsValidContentTypeFromSettings = function ( + type: string | undefined, + customWhiteList: string, + customBlackList: string, +): boolean { const blackList = fileUploadMediaBlackList(customBlackList); const whiteList = fileUploadMediaWhiteList(customWhiteList); - if (!type && blackList) { + if (blackList && type && isTypeOnList(type, blackList)) { return false; } - if (blackList && isTypeOnList(type, blackList)) { - return false; + if (whiteList) { + return isTypeOnList(type, whiteList); } if (!whiteList) { return true; } - return !!isTypeOnList(type, whiteList); + return false; }; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 4eb357fe9ee6a..b10a632463e71 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.8.0-develop" + "version": "6.10.0-develop" } diff --git a/apps/meteor/app/utils/server/restrictions.ts b/apps/meteor/app/utils/server/restrictions.ts index ca524b09d3510..6eb1c9a655d4a 100644 --- a/apps/meteor/app/utils/server/restrictions.ts +++ b/apps/meteor/app/utils/server/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/server'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts index fb55d478b6f5f..4cca28f1d5a94 100644 --- a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts @@ -3,6 +3,7 @@ import semver from 'semver'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; @@ -37,7 +38,8 @@ export const buildVersionUpdateMessage = async ( continue; } - await Settings.updateValueById('Update_LatestAvailableVersion', version.version); + (await Settings.updateValueById('Update_LatestAvailableVersion', version.version)).modifiedCount && + void notifyOnSettingChangedById('Update_LatestAvailableVersion'); await sendMessagesToAdmins({ msgs: async ({ adminUser }) => [ diff --git a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts index 960379d1a99b0..5ffebcfbbd5a7 100644 --- a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts +++ b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts @@ -2,6 +2,8 @@ import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; + declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -17,5 +19,13 @@ Meteor.methods({ } await Users.setBannerReadById(userId, id); + + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { + [`banners.${id}.read`]: true, + }, + }); }, }); diff --git a/apps/meteor/app/voip/server/startup.ts b/apps/meteor/app/voip/server/startup.ts index d96206f129856..1c27b913d103c 100644 --- a/apps/meteor/app/voip/server/startup.ts +++ b/apps/meteor/app/voip/server/startup.ts @@ -1,13 +1,13 @@ -import { Voip } from '@rocket.chat/core-services'; +import { VoipAsterisk } from '@rocket.chat/core-services'; import { settings } from '../../settings/server'; settings.watch('VoIP_Enabled', async (value: boolean) => { try { if (value) { - await Voip.init(); + await VoipAsterisk.init(); } else { - await Voip.stop(); + await VoipAsterisk.stop(); } } catch (e) { // do nothing @@ -21,7 +21,7 @@ settings.changeMultiple( // So to avoid stopping/starting voip too often, we debounce the call and restart 1 second after the last setting has reached us. if (settings.get('VoIP_Enabled')) { try { - await Voip.refresh(); + await VoipAsterisk.refresh(); } catch (e) { // do nothing } diff --git a/apps/meteor/ee/client/apps/@types/IOrchestrator.ts b/apps/meteor/client/apps/@types/IOrchestrator.ts similarity index 100% rename from apps/meteor/ee/client/apps/@types/IOrchestrator.ts rename to apps/meteor/client/apps/@types/IOrchestrator.ts diff --git a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js b/apps/meteor/client/apps/RealAppsEngineUIHost.js similarity index 78% rename from apps/meteor/ee/client/apps/RealAppsEngineUIHost.js rename to apps/meteor/client/apps/RealAppsEngineUIHost.js index bcd1a254a4bd3..4377f7c66abaa 100644 --- a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.js @@ -1,11 +1,11 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; import { Meteor } from 'meteor/meteor'; -import { ChatRoom } from '../../../app/models/client'; -import { getUserAvatarURL } from '../../../app/utils/client/getUserAvatarURL'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { RoomManager } from '../../../client/lib/RoomManager'; -import { baseURI } from '../../../client/lib/baseURI'; +import { ChatRoom } from '../../app/models/client'; +import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { RoomManager } from '../lib/RoomManager'; +import { baseURI } from '../lib/baseURI'; export class RealAppsEngineUIHost extends AppsEngineUIHost { constructor() { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/client/apps/gameCenter/GameCenter.tsx similarity index 87% rename from apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx rename to apps/meteor/client/apps/gameCenter/GameCenter.tsx index 75f4882ce7476..3261d1e1c51e3 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenter.tsx @@ -3,8 +3,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState } from 'react'; import type { ReactElement } from 'react'; -import { preventSyntheticEvent } from '../../../../client/lib/utils/preventSyntheticEvent'; -import { useRoomToolbox } from '../../../../client/views/room/contexts/RoomToolboxContext'; +import { preventSyntheticEvent } from '../../lib/utils/preventSyntheticEvent'; +import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; import { useExternalComponentsQuery } from './hooks/useExternalComponentsQuery'; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx similarity index 95% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index 1f37e5d6358a2..f589dd21ed502 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -9,7 +9,7 @@ import { ContextualbarBack, ContextualbarContent, ContextualbarClose, -} from '../../../../client/components/Contextualbar'; +} from '../../components/Contextualbar'; import type { IGame } from './GameCenter'; interface IGameCenterContainerProps { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx similarity index 79% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index e7afa0d9e6897..d0dcc6fad4fee 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -4,11 +4,11 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; -import UserAutoCompleteMultipleFederated from '../../../../client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useOpenedRoom } from '../../../../client/lib/RoomManager'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; +import GenericModal from '../../components/GenericModal'; +import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useOpenedRoom } from '../../lib/RoomManager'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling'; import type { IGame } from './GameCenter'; type Username = Exclude; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx similarity index 91% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterList.tsx index f45ba934ba3bb..58a4f05f5362c 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx @@ -3,13 +3,8 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { - ContextualbarHeader, - ContextualbarTitle, - ContextualbarClose, - ContextualbarContent, -} from '../../../../client/components/Contextualbar'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; +import { ContextualbarHeader, ContextualbarTitle, ContextualbarClose, ContextualbarContent } from '../../components/Contextualbar'; +import { FormSkeleton } from '../../components/Skeleton'; import type { IGame } from './GameCenter'; import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; diff --git a/apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts b/apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts similarity index 100% rename from apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts rename to apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts diff --git a/apps/meteor/ee/client/apps/orchestrator.ts b/apps/meteor/client/apps/orchestrator.ts similarity index 92% rename from apps/meteor/ee/client/apps/orchestrator.ts rename to apps/meteor/client/apps/orchestrator.ts index a921c8cb61bea..f33807d25be4d 100644 --- a/apps/meteor/ee/client/apps/orchestrator.ts +++ b/apps/meteor/client/apps/orchestrator.ts @@ -1,17 +1,18 @@ import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; +import type { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; -import { hasAtLeastOnePermission } from '../../../app/authorization/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import type { App } from '../../../client/views/marketplace/types'; +import { hasAtLeastOnePermission } from '../../app/authorization/client'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { dispatchToastMessage } from '../lib/toast'; +import type { App } from '../views/marketplace/types'; import type { IAppExternalURL, ICategory } from './@types/IOrchestrator'; import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; class AppClientOrchestrator { - private _appClientUIHost: RealAppsEngineUIHost; + private _appClientUIHost: AppsEngineUIHost; private _manager: AppClientManager; diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index 214bffc9841e8..99af9a1f6a2c2 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -1,4 +1,4 @@ -import { Option, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; +import { CheckOption, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -65,7 +65,13 @@ const AutoCompleteDepartmentMultiple = ({ return loadMoreDepartments(start, Math.min(50, departmentsTotal)); } } - renderItem={({ label, ...props }) =>