diff --git a/.changeset/afraid-guests-jog.md b/.changeset/afraid-guests-jog.md
deleted file mode 100644
index 420b9bb5d329..000000000000
--- a/.changeset/afraid-guests-jog.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/livechat": minor
----
-
-Created a `transferChat` Livechat API endpoint for transferring chats programmatically, the endpoint has all the limitations & permissions required that transferring via UI has
diff --git a/.changeset/bright-humans-cross.md b/.changeset/bright-humans-cross.md
new file mode 100644
index 000000000000..aa0c4c658994
--- /dev/null
+++ b/.changeset/bright-humans-cross.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+Federation actions like sending message in a federated DM, reacting in a federated chat, etc, will no longer work if the configuration is invalid.
diff --git a/.changeset/bump-patch-1722087664914.md b/.changeset/bump-patch-1722087664914.md
deleted file mode 100644
index e1eaa7980afb..000000000000
--- a/.changeset/bump-patch-1722087664914.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1722559871139.md b/.changeset/bump-patch-1722559871139.md
deleted file mode 100644
index e1eaa7980afb..000000000000
--- a/.changeset/bump-patch-1722559871139.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Bump @rocket.chat/meteor version.
diff --git a/.changeset/calm-tigers-peel.md b/.changeset/calm-tigers-peel.md
new file mode 100644
index 000000000000..32feed6f7107
--- /dev/null
+++ b/.changeset/calm-tigers-peel.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/ui-kit": patch
+---
+
+fix UiKit error message: Failed to resolve module: @rocket.chat/icons
diff --git a/.changeset/chatty-hounds-hammer.md b/.changeset/chatty-hounds-hammer.md
deleted file mode 100644
index 1a2d3a7de559..000000000000
--- a/.changeset/chatty-hounds-hammer.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/fuselage-ui-kit": patch
----
-
-Fix validations from "UiKit" modal component
diff --git a/.changeset/chilled-yaks-beg.md b/.changeset/chilled-yaks-beg.md
deleted file mode 100644
index 670fa24887b7..000000000000
--- a/.changeset/chilled-yaks-beg.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed issue in Marketplace that caused a subscription app to show incorrect modals when subscribing
diff --git a/.changeset/chilly-papayas-march.md b/.changeset/chilly-papayas-march.md
deleted file mode 100644
index a7724b126695..000000000000
--- a/.changeset/chilly-papayas-march.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed SAML users' full names being updated on login regardless of the "Overwrite user fullname (use idp attribute)" setting
diff --git a/.changeset/cool-rocks-remember.md b/.changeset/cool-rocks-remember.md
new file mode 100644
index 000000000000..97af36e94320
--- /dev/null
+++ b/.changeset/cool-rocks-remember.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+"@rocket.chat/model-typings": patch
+---
+
+Fixed login with third-party apps not working without the "Manage OAuth Apps" permission
diff --git a/.changeset/cuddly-brooms-approve.md b/.changeset/cuddly-brooms-approve.md
deleted file mode 100644
index 24905bb91c62..000000000000
--- a/.changeset/cuddly-brooms-approve.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/i18n": minor
----
-
-Allows admins to customize the `Subject` field of Omnichannel email transcripts via setting. By passing a value to the setting `Custom email subject for transcript`, system will use it as the `Subject` field, unless a custom subject is passed when requesting a transcript. If there's no custom subject and setting value is empty, the current default value will be used
diff --git a/.changeset/dry-pumas-draw.md b/.changeset/dry-pumas-draw.md
deleted file mode 100644
index b66ca5157cd5..000000000000
--- a/.changeset/dry-pumas-draw.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/livechat": patch
----
-
-Fixed an issue that caused the widget to set the wrong department when using the setDepartment Livechat api endpoint in conjunction with a Livechat Trigger
diff --git a/.changeset/empty-readers-teach.md b/.changeset/empty-readers-teach.md
deleted file mode 100644
index b4bd075ef654..000000000000
--- a/.changeset/empty-readers-teach.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/tools": patch
-"@rocket.chat/account-service": patch
----
-
-Fixed an inconsistent evaluation of the `Accounts_LoginExpiration` setting over the codebase. In some places, it was being used as milliseconds while in others as days. Invalid values produced different results. A helper function was created to centralize the setting validation and the proper value being returned to avoid edge cases.
-Negative values may be saved on the settings UI panel but the code will interpret any negative, NaN or 0 value to the default expiration which is 90 days.
diff --git a/.changeset/empty-toys-smell.md b/.changeset/empty-toys-smell.md
new file mode 100644
index 000000000000..043d9c19567d
--- /dev/null
+++ b/.changeset/empty-toys-smell.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Federated users can no longer be deleted.
diff --git a/.changeset/fast-buttons-shake.md b/.changeset/fast-buttons-shake.md
deleted file mode 100644
index 6281fc9941ec..000000000000
--- a/.changeset/fast-buttons-shake.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': minor
----
-
-Fixed an issue where FCM actions did not respect environment's proxy settings
diff --git a/.changeset/fast-lobsters-turn.md b/.changeset/fast-lobsters-turn.md
new file mode 100644
index 000000000000..ff1d97ea7289
--- /dev/null
+++ b/.changeset/fast-lobsters-turn.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed an issue due to an endpoint pagination that was causing that when an agent have assigned more than 50 departments, the departments have a blank space instead of the name.
diff --git a/.changeset/funny-boats-guess.md b/.changeset/funny-boats-guess.md
new file mode 100644
index 000000000000..076acff98329
--- /dev/null
+++ b/.changeset/funny-boats-guess.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": minor
+"@rocket.chat/i18n": minor
+---
+
+Added a new Audit endpoint `audit/rooms.members` that allows users with `view-members-list-all-rooms` to fetch a list of the members of any room even if the user is not part of it.
diff --git a/.changeset/funny-snails-promise.md b/.changeset/funny-snails-promise.md
deleted file mode 100644
index bdd74a60b1e9..000000000000
--- a/.changeset/funny-snails-promise.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/livechat": patch
----
-
-livechat `setDepartment` livechat api fixes:
-- Changing department didn't reflect on the registration form in real time
-- Changing the department mid conversation didn't transfer the chat
-- Depending on the state of the department, it couldn't be set as default
-
diff --git a/.changeset/funny-wolves-tie.md b/.changeset/funny-wolves-tie.md
deleted file mode 100644
index e2364ccb05e5..000000000000
--- a/.changeset/funny-wolves-tie.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixed issue where bad word filtering was not working in the UI for messages
diff --git a/.changeset/gentle-bugs-think.md b/.changeset/gentle-bugs-think.md
new file mode 100644
index 000000000000..fc4738f3043a
--- /dev/null
+++ b/.changeset/gentle-bugs-think.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Prevent `processRoomAbandonment` callback from erroring out when a room was inactive during a day Business Hours was not configured for.
diff --git a/.changeset/giant-spiders-pay.md b/.changeset/giant-spiders-pay.md
new file mode 100644
index 000000000000..1798cd2baaee
--- /dev/null
+++ b/.changeset/giant-spiders-pay.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixes an issue where the Announcement modal with long words was adding a horizontal scrollbar
diff --git a/.changeset/gorgeous-hotels-attend.md b/.changeset/gorgeous-hotels-attend.md
new file mode 100644
index 000000000000..fd858d7ace86
--- /dev/null
+++ b/.changeset/gorgeous-hotels-attend.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Stopped non channel members from dragging and dropping files in a channel they do not belong
diff --git a/.changeset/grumpy-worms-appear.md b/.changeset/grumpy-worms-appear.md
deleted file mode 100644
index fb9fab77b24c..000000000000
--- a/.changeset/grumpy-worms-appear.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/i18n": patch
----
-
-Fixed wrong wording on a federation setting
diff --git a/.changeset/happy-peaches-nail.md b/.changeset/happy-peaches-nail.md
deleted file mode 100644
index 2dfb2151ced0..000000000000
--- a/.changeset/happy-peaches-nail.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed issue with livechat agents not being able to leave omnichannel rooms if joining after a room has been closed by the visitor (due to race conditions)
diff --git a/.changeset/hip-queens-taste.md b/.changeset/hip-queens-taste.md
deleted file mode 100644
index f1d7bb6f3f0e..000000000000
--- a/.changeset/hip-queens-taste.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": minor
----
-
-Added the possibility for apps to remove users from a room
diff --git a/.changeset/hungry-wombats-act.md b/.changeset/hungry-wombats-act.md
deleted file mode 100644
index 4e50b172e17e..000000000000
--- a/.changeset/hungry-wombats-act.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed an issue where non-encrypted attachments were not being downloaded
diff --git a/.changeset/large-geese-ring.md b/.changeset/large-geese-ring.md
new file mode 100644
index 000000000000..9b36edf1c02d
--- /dev/null
+++ b/.changeset/large-geese-ring.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+Replaces an outdated banner with the Bubble component in order to display retention policy warning
diff --git a/.changeset/large-vans-attack.md b/.changeset/large-vans-attack.md
deleted file mode 100644
index c1008b2ca06f..000000000000
--- a/.changeset/large-vans-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-fixed the contextual bar closing when editing thread messages instead of cancelling the message edit
diff --git a/.changeset/lucky-beds-glow.md b/.changeset/lucky-beds-glow.md
deleted file mode 100644
index 3e23797025e1..000000000000
--- a/.changeset/lucky-beds-glow.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@rocket.chat/ui-client': minor
-'@rocket.chat/i18n': minor
-'@rocket.chat/meteor': minor
----
-
-Feature Preview: New Navigation - `Header` and `Contextualbar` size improvements consistent with the new global `NavBar`
diff --git a/.changeset/lucky-countries-look.md b/.changeset/lucky-countries-look.md
deleted file mode 100644
index 79deda53edfc..000000000000
--- a/.changeset/lucky-countries-look.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixed the disappearance of some settings after navigation under network latency.
diff --git a/.changeset/many-tables-love.md b/.changeset/many-tables-love.md
deleted file mode 100644
index 8f37283c6a96..000000000000
--- a/.changeset/many-tables-love.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/model-typings": minor
----
-
-Fixed Livechat rooms being displayed in the Engagement Dashboard's "Channels" tab
diff --git a/.changeset/mean-hairs-move.md b/.changeset/mean-hairs-move.md
deleted file mode 100644
index c92293d6ae95..000000000000
--- a/.changeset/mean-hairs-move.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': minor
----
-
-Fixed an issue where adding `OVERWRITE_SETTING_` for any setting wasn't immediately taking effect sometimes, and needed a server restart to reflect.
diff --git a/.changeset/nasty-windows-smile.md b/.changeset/nasty-windows-smile.md
new file mode 100644
index 000000000000..e80ec3db27a9
--- /dev/null
+++ b/.changeset/nasty-windows-smile.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Allow apps to react/unreact to messages via bridge
diff --git a/.changeset/nervous-rockets-impress.md b/.changeset/nervous-rockets-impress.md
deleted file mode 100644
index 26e9276193de..000000000000
--- a/.changeset/nervous-rockets-impress.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixes Missing line breaks on Omnichannel Room Info Panel
diff --git a/.changeset/new-balloons-speak.md b/.changeset/new-balloons-speak.md
deleted file mode 100644
index 7d4e7cd3a57e..000000000000
--- a/.changeset/new-balloons-speak.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixed web client crashing on Firefox private window. Firefox disables access to service workers inside private windows. Rocket.Chat needs service workers to process E2EE encrypted files on rooms. These types of files won't be available inside private windows, but the rest of E2EE encrypted features should work normally
diff --git a/.changeset/new-mayflies-wait.md b/.changeset/new-mayflies-wait.md
new file mode 100644
index 000000000000..832db68cecd4
--- /dev/null
+++ b/.changeset/new-mayflies-wait.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Deactivating users who federated will now be permanent.
diff --git a/.changeset/new-scissors-love.md b/.changeset/new-scissors-love.md
deleted file mode 100644
index fb962407b353..000000000000
--- a/.changeset/new-scissors-love.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-'@rocket.chat/omnichannel-services': minor
-'@rocket.chat/pdf-worker': minor
-'@rocket.chat/core-services': minor
-'@rocket.chat/model-typings': minor
-'@rocket.chat/i18n': minor
-'@rocket.chat/meteor': minor
----
-
-Added system messages support for Omnichannel PDF transcripts and email transcripts. Currently these transcripts don't render system messages and is shown as an empty message in PDF/email. This PR adds this support for all valid livechat system messages.
-
-Also added a new setting under transcripts, to toggle the inclusion of system messages in email and PDF transcripts.
diff --git a/.changeset/nice-laws-eat.md b/.changeset/nice-laws-eat.md
deleted file mode 100644
index e99e4f219ef9..000000000000
--- a/.changeset/nice-laws-eat.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'rocketchat-services': minor
-'@rocket.chat/core-services': minor
-'@rocket.chat/model-typings': minor
-'@rocket.chat/ui-video-conf': minor
-'@rocket.chat/core-typings': minor
-'@rocket.chat/ui-contexts': minor
-'@rocket.chat/models': minor
-'@rocket.chat/ui-kit': minor
-'@rocket.chat/i18n': minor
-'@rocket.chat/meteor': minor
----
-
-New Feature: Video Conference Persistent Chat.
-This feature provides a discussion id for conference provider apps to store the chat messages exchanged during the conferences, so that those users may then access those messages again at any time through Rocket.Chat.
\ No newline at end of file
diff --git a/.changeset/ninety-hounds-exist.md b/.changeset/ninety-hounds-exist.md
new file mode 100644
index 000000000000..99882de12018
--- /dev/null
+++ b/.changeset/ninety-hounds-exist.md
@@ -0,0 +1,7 @@
+---
+'@rocket.chat/rest-typings': patch
+'@rocket.chat/meteor': patch
+'@rocket.chat/i18n': patch
+---
+
+Fix: Show correct user info actions for non-members in channels.
diff --git a/.changeset/perfect-coins-camp.md b/.changeset/perfect-coins-camp.md
deleted file mode 100644
index 4dbddf965742..000000000000
--- a/.changeset/perfect-coins-camp.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-fixed an issue in the "Create discussion" form, that would have the "Create" action button disabled even though the form is prefilled when opening it from the message action
diff --git a/.changeset/polite-foxes-repair.md b/.changeset/polite-foxes-repair.md
deleted file mode 100644
index 2f524c7e5f10..000000000000
--- a/.changeset/polite-foxes-repair.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': minor
----
-
-Added a method to the Apps-Engine that allows apps to read multiple messages from a room
diff --git a/.changeset/popular-trees-lay.md b/.changeset/popular-trees-lay.md
deleted file mode 100644
index f38ef1f92367..000000000000
--- a/.changeset/popular-trees-lay.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Removed 'Hide' option in the room menu for Omnichannel conversations.
diff --git a/.changeset/pre.json b/.changeset/pre.json
deleted file mode 100644
index 38a7fe160e48..000000000000
--- a/.changeset/pre.json
+++ /dev/null
@@ -1,114 +0,0 @@
-{
- "mode": "pre",
- "tag": "rc",
- "initialVersions": {
- "@rocket.chat/meteor": "6.11.0-develop",
- "rocketchat-services": "1.2.1",
- "@rocket.chat/account-service": "0.4.1",
- "@rocket.chat/authorization-service": "0.4.1",
- "@rocket.chat/ddp-streamer": "0.3.1",
- "@rocket.chat/omnichannel-transcript": "0.4.1",
- "@rocket.chat/presence-service": "0.4.1",
- "@rocket.chat/queue-worker": "0.4.1",
- "@rocket.chat/stream-hub-service": "0.4.1",
- "@rocket.chat/api-client": "0.2.1",
- "@rocket.chat/ddp-client": "0.3.1",
- "@rocket.chat/license": "0.2.1",
- "@rocket.chat/omnichannel-services": "0.2.1",
- "@rocket.chat/pdf-worker": "0.1.1",
- "@rocket.chat/presence": "0.2.1",
- "@rocket.chat/ui-theming": "0.2.0",
- "@rocket.chat/account-utils": "0.0.2",
- "@rocket.chat/agenda": "0.1.0",
- "@rocket.chat/apps": "0.1.1",
- "@rocket.chat/base64": "1.0.13",
- "@rocket.chat/cas-validate": "0.0.2",
- "@rocket.chat/core-services": "0.4.1",
- "@rocket.chat/core-typings": "6.11.0-develop",
- "@rocket.chat/cron": "0.1.1",
- "@rocket.chat/eslint-config": "0.7.0",
- "@rocket.chat/favicon": "0.0.2",
- "@rocket.chat/fuselage-ui-kit": "8.0.1",
- "@rocket.chat/gazzodown": "8.0.1",
- "@rocket.chat/i18n": "0.5.0",
- "@rocket.chat/instance-status": "0.1.1",
- "@rocket.chat/jwt": "0.1.1",
- "@rocket.chat/livechat": "1.18.1",
- "@rocket.chat/log-format": "0.0.2",
- "@rocket.chat/logger": "0.0.2",
- "@rocket.chat/message-parser": "0.31.29",
- "@rocket.chat/mock-providers": "0.1.0",
- "@rocket.chat/model-typings": "0.5.1",
- "@rocket.chat/models": "0.1.1",
- "@rocket.chat/poplib": "0.0.2",
- "@rocket.chat/password-policies": "0.0.2",
- "@rocket.chat/patch-injection": "0.0.1",
- "@rocket.chat/peggy-loader": "0.31.25",
- "@rocket.chat/random": "1.2.2",
- "@rocket.chat/release-action": "2.2.3",
- "@rocket.chat/release-changelog": "0.1.0",
- "@rocket.chat/rest-typings": "6.11.0-develop",
- "@rocket.chat/server-cloud-communication": "0.0.2",
- "@rocket.chat/server-fetch": "0.0.3",
- "@rocket.chat/sha256": "1.0.10",
- "@rocket.chat/tools": "0.2.1",
- "@rocket.chat/ui-avatar": "4.0.1",
- "@rocket.chat/ui-client": "8.0.1",
- "@rocket.chat/ui-composer": "0.2.0",
- "@rocket.chat/ui-contexts": "8.0.1",
- "@rocket.chat/ui-kit": "0.35.0",
- "@rocket.chat/ui-video-conf": "8.0.1",
- "@rocket.chat/uikit-playground": "0.3.1",
- "@rocket.chat/web-ui-registration": "8.0.1"
- },
- "changesets": [
- "afraid-guests-jog",
- "bump-patch-1722087664914",
- "bump-patch-1722559871139",
- "chatty-hounds-hammer",
- "chilled-yaks-beg",
- "chilly-papayas-march",
- "cuddly-brooms-approve",
- "dry-pumas-draw",
- "empty-readers-teach",
- "fast-buttons-shake",
- "funny-snails-promise",
- "funny-wolves-tie",
- "grumpy-worms-appear",
- "happy-peaches-nail",
- "hip-queens-taste",
- "hungry-wombats-act",
- "large-vans-attack",
- "lucky-beds-glow",
- "lucky-countries-look",
- "many-tables-love",
- "mean-hairs-move",
- "nervous-rockets-impress",
- "new-balloons-speak",
- "new-scissors-love",
- "nice-laws-eat",
- "perfect-coins-camp",
- "polite-foxes-repair",
- "popular-trees-lay",
- "proud-waves-bathe",
- "quick-ducks-live",
- "rare-penguins-hope",
- "red-numbers-happen",
- "red-vans-shave",
- "rich-carpets-brush",
- "rotten-eggs-end",
- "selfish-emus-sing",
- "shaggy-hats-raise",
- "sixty-nails-clean",
- "smooth-lobsters-flash",
- "soft-donkeys-thank",
- "sour-forks-breathe",
- "thin-windows-reply",
- "violet-brooms-press",
- "weak-insects-sort",
- "weak-pets-talk",
- "weak-taxis-design",
- "weak-tigers-suffer",
- "witty-bats-develop"
- ]
-}
diff --git a/.changeset/proud-waves-bathe.md b/.changeset/proud-waves-bathe.md
deleted file mode 100644
index 556fa3af80e1..000000000000
--- a/.changeset/proud-waves-bathe.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/model-typings": minor
----
-
-Improved Engagement Dashboard's "Channels" tab performance by not returning rooms that had no activity in the analyzed period
diff --git a/.changeset/proud-years-buy.md b/.changeset/proud-years-buy.md
new file mode 100644
index 000000000000..94f4ab0df736
--- /dev/null
+++ b/.changeset/proud-years-buy.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/i18n': patch
+---
+
+Fixes a typo in german translation and fixes the broken hyperlink for Resend and Change Email
diff --git a/.changeset/purple-dolls-serve.md b/.changeset/purple-dolls-serve.md
new file mode 100644
index 000000000000..fc44faa60a38
--- /dev/null
+++ b/.changeset/purple-dolls-serve.md
@@ -0,0 +1,7 @@
+---
+'@rocket.chat/web-ui-registration': patch
+'@rocket.chat/i18n': patch
+'@rocket.chat/meteor': patch
+---
+
+Fixes an issue where creating a new user with an invalid username (containing special characters) resulted in an error message, but the user was still created. The user creation process now properly aborts when an invalid username is provided.
diff --git a/.changeset/quick-ducks-live.md b/.changeset/quick-ducks-live.md
deleted file mode 100644
index ad628c13d087..000000000000
--- a/.changeset/quick-ducks-live.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed LDAP rooms, teams and roles syncs not being triggered on login even when the "Update User Data on Login" setting is enabled
diff --git a/.changeset/rare-penguins-hope.md b/.changeset/rare-penguins-hope.md
deleted file mode 100644
index 187bd9d09ddc..000000000000
--- a/.changeset/rare-penguins-hope.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/core-typings": patch
----
-
-Allow customFields on livechat creation bridge
diff --git a/.changeset/red-numbers-happen.md b/.changeset/red-numbers-happen.md
deleted file mode 100644
index 61cb0d2b7586..000000000000
--- a/.changeset/red-numbers-happen.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed "Copy link" message action enabled in Starred and Pinned list for End to End Encrypted channels, this action is disabled now
diff --git a/.changeset/red-vans-shave.md b/.changeset/red-vans-shave.md
deleted file mode 100644
index ddf76535087e..000000000000
--- a/.changeset/red-vans-shave.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Fixed issue that caused unintentional clicks when scrolling the channels sidebar on safari/chrome in iOS
diff --git a/.changeset/rich-carpets-brush.md b/.changeset/rich-carpets-brush.md
deleted file mode 100644
index 16741e31e54a..000000000000
--- a/.changeset/rich-carpets-brush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixed some anomalies related to disabled E2EE rooms. Earlier there are some weird issues with disabled E2EE rooms, this PR fixes these anomalies.
diff --git a/.changeset/rich-pillows-hang.md b/.changeset/rich-pillows-hang.md
new file mode 100644
index 000000000000..b714a5e6acd9
--- /dev/null
+++ b/.changeset/rich-pillows-hang.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixes the `expanded` prop being accidentally forwarded to `ContextualbarHeader`
diff --git a/.changeset/rooms-table-ts.md b/.changeset/rooms-table-ts.md
new file mode 100644
index 000000000000..b5055ad26f69
--- /dev/null
+++ b/.changeset/rooms-table-ts.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+Add "Created at" column to admin rooms table
diff --git a/.changeset/rotten-camels-pretend.md b/.changeset/rotten-camels-pretend.md
new file mode 100644
index 000000000000..5145bbaa5050
--- /dev/null
+++ b/.changeset/rotten-camels-pretend.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+"@rocket.chat/core-typings": patch
+---
+
+Fixed issue with system messages being counted as agents' first responses in livechat rooms (which caused the "best first response time" and "average first response time" metrics to be unreliable for all agents)
diff --git a/.changeset/rotten-eggs-end.md b/.changeset/rotten-eggs-end.md
deleted file mode 100644
index 7d0ad6ee5047..000000000000
--- a/.changeset/rotten-eggs-end.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/i18n": patch
-"@rocket.chat/ui-client": patch
----
-
-Implemented a new tab to the users page called 'Active', this tab lists all users who have logged in for the first time and are active.
diff --git a/.changeset/rude-dogs-burn.md b/.changeset/rude-dogs-burn.md
new file mode 100644
index 000000000000..e81f00782083
--- /dev/null
+++ b/.changeset/rude-dogs-burn.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed a behavior when updating messages that prevented the `customFields` prop from being updated if there were no changes to the `msg` property. Now, `customFields` will be always updated on message update even if `msg` doesn't change
diff --git a/.changeset/selfish-emus-sing.md b/.changeset/selfish-emus-sing.md
deleted file mode 100644
index 315d674a1857..000000000000
--- a/.changeset/selfish-emus-sing.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/i18n": minor
----
-
-Added account setting `Accounts_Default_User_Preferences_sidebarSectionsOrder` to allow users to reorganize sidebar sections
diff --git a/.changeset/shaggy-hats-raise.md b/.changeset/shaggy-hats-raise.md
deleted file mode 100644
index 40ee9f8fbb55..000000000000
--- a/.changeset/shaggy-hats-raise.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": minor
----
-
-Added a new setting `Livechat_transcript_send_always` that allows admins to decide if email transcript should be sent all the times when a conversation is closed. This setting bypasses agent's preferences. For this setting to work, `Livechat_enable_transcript` should be off, meaning that visitors will no longer receive the option to decide if they want a transcript or not.
diff --git a/.changeset/six-beers-fry.md b/.changeset/six-beers-fry.md
new file mode 100644
index 000000000000..48409c2f8de5
--- /dev/null
+++ b/.changeset/six-beers-fry.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+New button added to validate Matrix Federation configuration. A new field inside admin settings will reflect the configuration status being either 'Valid' or 'Invalid'.
diff --git a/.changeset/sixty-nails-clean.md b/.changeset/sixty-nails-clean.md
deleted file mode 100644
index 7d13e02f0bd3..000000000000
--- a/.changeset/sixty-nails-clean.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixed an issue that prevented the option to start a discussion from being shown on the message actions
diff --git a/.changeset/sixty-spoons-own.md b/.changeset/sixty-spoons-own.md
new file mode 100644
index 000000000000..0b717c3965ef
--- /dev/null
+++ b/.changeset/sixty-spoons-own.md
@@ -0,0 +1,9 @@
+---
+"@rocket.chat/meteor": minor
+"@rocket.chat/core-typings": minor
+"@rocket.chat/model-typings": minor
+"@rocket.chat/models": minor
+"@rocket.chat/rest-typings": minor
+---
+
+Introduced "create contacts" endpoint to omnichannel
diff --git a/.changeset/smart-mice-attack.md b/.changeset/smart-mice-attack.md
new file mode 100644
index 000000000000..3ca47060ce5c
--- /dev/null
+++ b/.changeset/smart-mice-attack.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed an issue where teams were being created with no room associated with it.
diff --git a/.changeset/smooth-lobsters-flash.md b/.changeset/smooth-lobsters-flash.md
deleted file mode 100644
index 541d5069ee9c..000000000000
--- a/.changeset/smooth-lobsters-flash.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fix show correct user roles after updating user roles on admin edit user panel.
diff --git a/.changeset/soft-donkeys-thank.md b/.changeset/soft-donkeys-thank.md
deleted file mode 100644
index 7273ddcffca4..000000000000
--- a/.changeset/soft-donkeys-thank.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/mock-providers": patch
-"@rocket.chat/ui-contexts": patch
-"@rocket.chat/web-ui-registration": patch
----
-
-Fixed an issue with blocked login when dismissed 2FA modal by clicking outside of it or pressing the escape key
diff --git a/.changeset/sour-forks-breathe.md b/.changeset/sour-forks-breathe.md
deleted file mode 100644
index 2d1076845fa9..000000000000
--- a/.changeset/sour-forks-breathe.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": minor
----
-
-Extended apps-engine events for users leaving a room to also fire when being removed by another user. Also added the triggering user's information to the event's context payload.
diff --git a/.changeset/spicy-kings-think.md b/.changeset/spicy-kings-think.md
new file mode 100644
index 000000000000..9e8f3648b28c
--- /dev/null
+++ b/.changeset/spicy-kings-think.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixes multiple problems with the `processRoomAbandonment` hook. This hook is in charge of calculating the time a room has been abandoned (this means, the time that elapsed since a room was last answered by an agent until it was closed). However, when business hours were enabled and the user didn't open on one day, if an abandoned room happened to be abandoned _over_ the day there was no business hour configuration, then the process will error out.
+Additionally, the values the code was calculating were not right. When business hours are enabled, this code should only count the abandonment time _while a business hour was open_. When rooms were left abandoned for days or weeks, this will also throw an error or output an invalid count.
diff --git a/.changeset/strong-terms-love.md b/.changeset/strong-terms-love.md
new file mode 100644
index 000000000000..2535a466eb8e
--- /dev/null
+++ b/.changeset/strong-terms-love.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+"@rocket.chat/model-typings": patch
+---
+
+Fixed issue with livechat analytics in a given date range considering conversation data from the following day
diff --git a/.changeset/stupid-fishes-relate.md b/.changeset/stupid-fishes-relate.md
new file mode 100644
index 000000000000..82bfaa1cfd28
--- /dev/null
+++ b/.changeset/stupid-fishes-relate.md
@@ -0,0 +1,7 @@
+---
+'@rocket.chat/core-typings': minor
+'@rocket.chat/i18n': minor
+'@rocket.chat/meteor': minor
+---
+
+Added a new setting to enable/disable file encryption in an end to end encrypted room.
diff --git a/.changeset/stupid-pigs-share.md b/.changeset/stupid-pigs-share.md
new file mode 100644
index 000000000000..55d68c66d587
--- /dev/null
+++ b/.changeset/stupid-pigs-share.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+Wraps some room settings in an accordion advanced settings section in room edit contextual bar to improve organization
diff --git a/.changeset/ten-bulldogs-clap.md b/.changeset/ten-bulldogs-clap.md
new file mode 100644
index 000000000000..15f88bb6bd97
--- /dev/null
+++ b/.changeset/ten-bulldogs-clap.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+fixed an issue with the "follow message" button not changing state after click
diff --git a/.changeset/thin-windows-reply.md b/.changeset/thin-windows-reply.md
deleted file mode 100644
index 1a32e1ddebfb..000000000000
--- a/.changeset/thin-windows-reply.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Fixes an issue not displaying all groups in settings list
diff --git a/.changeset/twelve-windows-train.md b/.changeset/twelve-windows-train.md
new file mode 100644
index 000000000000..4c6ef548e650
--- /dev/null
+++ b/.changeset/twelve-windows-train.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixed: Custom fields in extraData now correctly added to extraRoomInfo by livechat.beforeRoom callback during livechat room creation.
diff --git a/.changeset/two-bikes-crash.md b/.changeset/two-bikes-crash.md
new file mode 100644
index 000000000000..a120435e4a48
--- /dev/null
+++ b/.changeset/two-bikes-crash.md
@@ -0,0 +1,7 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixed an issue related to setting Accounts_ForgetUserSessionOnWindowClose, this setting was not working as expected.
+
+The new meteor 2.16 release introduced a new option to configure the Accounts package and choose between the local storage or session storage. They also changed how Meteor.\_localstorage works internally. Due to these changes in Meteor, our setting to use session storage wasn't working as expected. This PR fixes this issue and configures the Accounts package according to the workspace settings.
diff --git a/.changeset/violet-brooms-press.md b/.changeset/violet-brooms-press.md
deleted file mode 100644
index 632026d6fe2e..000000000000
--- a/.changeset/violet-brooms-press.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': patch
----
-
-Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)
diff --git a/.changeset/violet-radios-begin.md b/.changeset/violet-radios-begin.md
new file mode 100644
index 000000000000..d11f23b47478
--- /dev/null
+++ b/.changeset/violet-radios-begin.md
@@ -0,0 +1,15 @@
+---
+'@rocket.chat/core-typings': minor
+'@rocket.chat/i18n': minor
+'@rocket.chat/meteor': minor
+---
+
+Fixed a bug related to uploading end to end encrypted file.
+
+E2EE files and uploads are uploaded as files of mime type `application/octet-stream` as we can't reveal the mime type of actual content since it is encrypted and has to be kept confidential.
+
+The server resolves the mime type of encrypted file as `application/octet-stream` but it wasn't playing nicely with existing settings related to whitelisted and blacklisted media types.
+
+E2EE files upload was getting blocked if `application/octet-stream` is not a whitelisted media type.
+
+Now this PR solves this issue by always accepting E2EE uploads even if `application/octet-stream` is not whitelisted but it will block the upload if `application/octet-stream` is black listed.
diff --git a/.changeset/weak-insects-sort.md b/.changeset/weak-insects-sort.md
deleted file mode 100644
index cbbe7c4aa08c..000000000000
--- a/.changeset/weak-insects-sort.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@rocket.chat/meteor": patch
----
-
-Improving UX by change the position of room info actions buttons and menu order to avoid missclick in destructive actions.
diff --git a/.changeset/weak-pets-talk.md b/.changeset/weak-pets-talk.md
deleted file mode 100644
index abaa9c683d65..000000000000
--- a/.changeset/weak-pets-talk.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@rocket.chat/omnichannel-services': patch
-'@rocket.chat/core-services': patch
-'@rocket.chat/meteor': patch
----
-
-Reduced time on generation of PDF transcripts. Earlier Rocket.Chat was fetching the required translations everytime a PDF transcript was requested, this process was async and was being unnecessarily being performed on every pdf transcript request. This PR improves this and now the translations are loaded at the start and kept in memory to process further pdf transcripts requests. This reduces the time of asynchronously fetching translations again and again.
diff --git a/.changeset/weak-taxis-design.md b/.changeset/weak-taxis-design.md
deleted file mode 100644
index a2d435495cd7..000000000000
--- a/.changeset/weak-taxis-design.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@rocket.chat/meteor': minor
----
-
-Added handling of attachments in Omnichannel email transcripts. Earlier attachments were being skipped and were being shown as empty space, now it should render the image attachments and should show relevant error message for unsupported attachments.
diff --git a/.changeset/weak-tigers-suffer.md b/.changeset/weak-tigers-suffer.md
deleted file mode 100644
index 91748a43c677..000000000000
--- a/.changeset/weak-tigers-suffer.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-"@rocket.chat/meteor": minor
-"@rocket.chat/model-typings": minor
-"@rocket.chat/rest-typings": minor
----
-
-Added the ability to filter chats by `queued` on the Current Chats Omnichannel page
diff --git a/.changeset/witty-bats-develop.md b/.changeset/witty-bats-develop.md
deleted file mode 100644
index 42c9409d9ef3..000000000000
--- a/.changeset/witty-bats-develop.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@rocket.chat/meteor": patch
-"@rocket.chat/apps": patch
-"@rocket.chat/core-services": patch
-"@rocket.chat/core-typings": patch
-"@rocket.chat/fuselage-ui-kit": patch
-"@rocket.chat/rest-typings": patch
-"@rocket.chat/ddp-streamer": patch
-"@rocket.chat/presence": patch
-"rocketchat-services": patch
----
-
-Added the `user` param to apps-engine update method call, allowing apps' new `onUpdate` hook to know who triggered the update.
diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml
index dbc615da889a..5af39b924057 100644
--- a/.github/actions/build-docker/action.yml
+++ b/.github/actions/build-docker/action.yml
@@ -17,13 +17,28 @@ inputs:
required: false
description: 'Containers to build along with Rocket.Chat'
type: string
+ turbo-cache:
+ required: false
+ description: 'Enable turbo cache'
+ default: 'true'
+ publish-image:
+ required: false
+ description: 'Publish image'
+ default: 'true'
+ setup:
+ required: false
+ description: 'Setup node.js'
+ default: 'true'
+ NPM_TOKEN:
+ required: false
+ description: 'NPM token'
runs:
using: composite
steps:
- 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')
+ if: inputs.publish-image == 'true' &&(github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
uses: docker/login-action@v2
with:
registry: ghcr.io
@@ -31,7 +46,7 @@ runs:
password: ${{ inputs.CR_PAT }}
- name: Restore build
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: build
path: /tmp/build
@@ -42,17 +57,21 @@ runs:
cd /tmp/build
tar xzf Rocket.Chat.tar.gz
rm Rocket.Chat.tar.gz
-
- uses: rharkor/caching-for-turbo@v1.5
+ # if we are testing a PR from a fork, we already called the turbo cache at this point, so it should be false
+ if: inputs.turbo-cache == 'true'
- name: Setup NodeJS
uses: ./.github/actions/setup-node
+ if: inputs.setup == 'true'
with:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ inputs.NPM_TOKEN }}
- run: yarn build
+ if: inputs.setup == 'true'
shell: bash
- name: Build Docker images
@@ -63,9 +82,14 @@ runs:
docker compose -f docker-compose-ci.yml build "${args[@]}"
- name: Publish Docker images to GitHub Container Registry
- if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
+ if: inputs.publish-image == 'true' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
shell: bash
run: |
args=(rocketchat ${{ inputs.build-containers }})
docker compose -f docker-compose-ci.yml push "${args[@]}"
+
+ - name: Clean up temporary files
+ shell: bash
+ run: |
+ sudo rm -rf /tmp/bundle
diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml
index c13703aeea46..525595146700 100644
--- a/.github/actions/meteor-build/action.yml
+++ b/.github/actions/meteor-build/action.yml
@@ -13,6 +13,9 @@ inputs:
required: true
description: 'Node version'
type: string
+ NPM_TOKEN:
+ required: false
+ description: 'NPM token'
runs:
using: composite
@@ -29,6 +32,7 @@ runs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ inputs.NPM_TOKEN }}
# - name: Free disk space
# run: |
@@ -124,7 +128,8 @@ runs:
tar czf /tmp/Rocket.Chat.tar.gz bundle
- name: Store build
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: build
path: /tmp/Rocket.Chat.tar.gz
+ overwrite: true
diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml
index caa3c63e00f0..1035e2835792 100644
--- a/.github/actions/setup-node/action.yml
+++ b/.github/actions/setup-node/action.yml
@@ -1,22 +1,27 @@
name: 'Setup Node'
+description: 'Setup NodeJS'
inputs:
node-version:
required: true
- type: string
+ description: 'Node version'
cache-modules:
required: false
- type: boolean
+ description: 'Cache node_modules'
install:
required: false
- type: boolean
+ description: 'Install dependencies'
deno-dir:
required: false
- type: string
+ description: 'Deno directory'
default: ~/.deno-cache
+ NPM_TOKEN:
+ required: false
+ description: 'NPM token'
outputs:
node-version:
+ description: 'Node version'
value: ${{ steps.node-version.outputs.node-version }}
runs:
@@ -32,6 +37,7 @@ runs:
uses: actions/cache@v3
with:
path: |
+ .turbo/cache
node_modules
${{ env.DENO_DIR }}
apps/meteor/node_modules
@@ -48,6 +54,13 @@ runs:
node-version: ${{ inputs.node-version }}
cache: 'yarn'
+ - name: yarn login
+ shell: bash
+ if: inputs.NPM_TOKEN
+ run: |
+ echo "//registry.npmjs.org/:_authToken=${{ inputs.NPM_TOKEN }}" > ~/.npmrc
+
- name: yarn install
+ if: inputs.install
shell: bash
run: yarn
diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml
index fd214bc39488..af50b3230ba7 100644
--- a/.github/workflows/ci-code-check.yml
+++ b/.github/workflows/ci-code-check.yml
@@ -35,6 +35,7 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# - name: Free disk space
# run: |
diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml
index 8bbb40a734fb..e6c02b7b6417 100644
--- a/.github/workflows/ci-test-e2e.yml
+++ b/.github/workflows/ci-test-e2e.yml
@@ -130,7 +130,11 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - uses: rharkor/caching-for-turbo@v1.5
+
+ - run: yarn build
# if we are testing a PR from a fork, we need to build the docker image at this point
- uses: ./.github/actions/build-docker
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
@@ -138,8 +142,12 @@ jobs:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
node-version: ${{ inputs.node-version }}
-
- - uses: rharkor/caching-for-turbo@v1.5
+ # we already called the turbo cache at this point, so it should be false
+ turbo-cache: false
+ # the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it
+ publish-image: false
+ setup: false
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Start httpbin container and wait for it to be ready
if: inputs.type == 'api'
@@ -159,8 +167,6 @@ jobs:
exit 1
fi
- - run: yarn build
-
- name: Prepare code coverage directory
if: inputs.release == 'ee'
run: |
@@ -187,12 +193,6 @@ jobs:
run: |
docker compose -f docker-compose-ci.yml up -d
- - name: Clean up temporary files
- # remove all folders inside /tmp except /tmp/coverage
- run: |
- cd /tmp
- sudo find . -mindepth 1 -maxdepth 1 -type d | grep -v './coverage' | sudo xargs rm -rf
-
- name: Cache Playwright binaries
if: inputs.type == 'ui'
uses: actions/cache@v3
@@ -289,9 +289,9 @@ jobs:
- name: Store playwright test trace
if: inputs.type == 'ui' && always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: playwright-test-trace${{ inputs.release }}
+ name: playwright-test-trace-${{ matrix.mongodb-version }}-${{ matrix.shard }}
path: ./apps/meteor/tests/e2e/.playwright*
- name: Show server logs if E2E test failed
@@ -322,14 +322,14 @@ jobs:
- name: Store e2e-api-ee-coverage
if: inputs.type == 'api' && inputs.release == 'ee'
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: e2e-api-ee-coverage
+ name: e2e-api-ee-coverage-${{ matrix.mongodb-version }}-${{ matrix.shard }}
path: /tmp/coverage
- name: Store e2e-ee-coverage
if: inputs.type == 'ui' && inputs.release == 'ee'
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: e2e-ee-coverage
+ name: e2e-ee-coverage-${{ matrix.mongodb-version }}-${{ matrix.shard }}
path: ./apps/meteor/coverage*
diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml
index a32c1e575b8f..840808ff5e31 100644
--- a/.github/workflows/ci-test-unit.yml
+++ b/.github/workflows/ci-test-unit.yml
@@ -39,6 +39,7 @@ jobs:
node-version: ${{ inputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: rharkor/caching-for-turbo@v1.5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 77a8d648ae61..514dd6d1c518 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -152,6 +152,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Cache vite
uses: actions/cache@v3
@@ -253,6 +254,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
platform: ${{ matrix.platform }}
build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
build-gh-docker:
name: đĸ Build Docker Images for Production
@@ -280,6 +282,7 @@ jobs:
node-version: ${{ needs.release-versions.outputs.node-version }}
platform: ${{ matrix.platform }}
build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Rename official Docker tag to GitHub Container Registry
if: matrix.platform == 'official'
@@ -492,7 +495,7 @@ jobs:
ref: ${{ github.ref }}
- name: Restore build
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: build
path: /tmp/build
@@ -540,7 +543,7 @@ jobs:
- uses: actions/checkout@v4
- name: Restore build
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: build
path: /tmp/build
@@ -560,6 +563,7 @@ jobs:
release: preview
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_PAT }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
docker-image-publish:
name: đ Publish Docker Image (main)
@@ -576,13 +580,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.CR_USER }}
@@ -683,13 +687,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.CR_USER }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 483b404a6dc8..202a02dd7785 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -26,7 +26,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages
with:
languages: javascript
@@ -34,7 +34,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
@@ -48,4 +48,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml
index 5ef8027b1467..b2eae5d90b92 100644
--- a/.github/workflows/new-release.yml
+++ b/.github/workflows/new-release.yml
@@ -37,6 +37,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: rharkor/caching-for-turbo@v1.5
diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml
index bc9d1f042d58..d8f6db97c455 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.4.1
+ - uses: thehanimo/pr-title-checker@v1.4.2
with:
GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }}
diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml
index e792127eac9d..084f2a383480 100644
--- a/.github/workflows/pr-update-description.yml
+++ b/.github/workflows/pr-update-description.yml
@@ -24,6 +24,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: rharkor/caching-for-turbo@v1.5
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index ccc3408e194e..3f2067ac7ec3 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -27,6 +27,7 @@ jobs:
node-version: 14.21.3
cache-modules: true
install: true
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: rharkor/caching-for-turbo@v1.5
diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml
index e52b4870b369..90c835577dc1 100644
--- a/.github/workflows/update-version-durability.yml
+++ b/.github/workflows/update-version-durability.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js
- uses: actions/setup-node@v3.7.0
+ uses: actions/setup-node@v4.0.3
with:
node-version: '20.15.1'
diff --git a/.gitignore b/.gitignore
index fcf2b8cd07c7..4e6e4bb29da9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,6 @@ yarn-error.log*
*.sublime-workspace
**/.vim/
+
+data/
+registration.yaml
diff --git a/_templates/package/new/jest.config.ts.t b/_templates/package/new/jest.config.ts.t
new file mode 100644
index 000000000000..c18c8ae02465
--- /dev/null
+++ b/_templates/package/new/jest.config.ts.t
@@ -0,0 +1,6 @@
+import server from '@rocket.chat/jest-presets/server';
+import type { Config } from 'jest';
+
+export default {
+ preset: server.preset,
+} satisfies Config;
diff --git a/_templates/package/new/package.json.ejs.t b/_templates/package/new/package.json.ejs.t
index 950e5cb2bf62..6bee52f55927 100644
--- a/_templates/package/new/package.json.ejs.t
+++ b/_templates/package/new/package.json.ejs.t
@@ -7,11 +7,11 @@ to: packages/<%= name %>/package.json
"version": "0.0.1",
"private": true,
"devDependencies": {
- "@types/jest": "~29.5.3",
+ "@rocket.chat/jest-presets": "workspace:~",
+ "@types/jest": "~29.5.12",
"eslint": "~8.45.0",
- "jest": "~29.6.1",
- "ts-jest": "~29.0.5",
- "typescript": "~5.1.6"
+ "jest": "~29.7.0",
+ "typescript": "~5.3.3"
},
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
diff --git a/_templates/package/new/tsconfig.json.ejs.t b/_templates/package/new/tsconfig.json.ejs.t
index 3e192c674d1b..399544502ed0 100644
--- a/_templates/package/new/tsconfig.json.ejs.t
+++ b/_templates/package/new/tsconfig.json.ejs.t
@@ -2,10 +2,10 @@
to: packages/<%= name %>/tsconfig.json
---
{
- "extends": "../../tsconfig.base.client.json",
+ "extends": "../../tsconfig.base.server.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
- "include": ["./src/**/*"]
+ "include": ["./src/**/*"],
}
diff --git a/apps/meteor/.mocharc.client.js b/apps/meteor/.mocharc.client.js
deleted file mode 100644
index cf339a420378..000000000000
--- a/apps/meteor/.mocharc.client.js
+++ /dev/null
@@ -1,42 +0,0 @@
-'use strict';
-
-/**
- * Mocha configuration for client-side unit and integration tests.
- */
-
-const base = require('./.mocharc.base.json');
-
-/**
- * Mocha will run `ts-node` without doing type checking to speed-up the tests. It should be fine as `npm run typecheck`
- * covers test files too.
- */
-
-Object.assign(
- process.env,
- {
- TS_NODE_FILES: true,
- TS_NODE_TRANSPILE_ONLY: true,
- },
- process.env,
-);
-
-module.exports = {
- ...base, // see https://github.com/mochajs/mocha/issues/3916
- require: [
- ...base.require,
- './tests/setup/registerWebApiMocks.ts',
- './tests/setup/hoistedReact.ts',
- './tests/setup/cleanupTestingLibrary.ts',
- ],
- reporter: 'dot',
- timeout: 5000,
- exit: false,
- slow: 200,
- spec: [
- 'tests/unit/client/sidebar/**/*.spec.{ts,tsx}',
- 'tests/unit/client/components/**/*.spec.{ts,tsx}',
- 'tests/unit/client/lib/**/*.spec.{ts,tsx}',
- 'tests/unit/lib/**/*.tests.ts',
- 'tests/unit/client/**/*.test.ts',
- ],
-};
diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md
index c80c36bf4890..f466c34da838 100644
--- a/apps/meteor/CHANGELOG.md
+++ b/apps/meteor/CHANGELOG.md
@@ -1,5 +1,276 @@
# @rocket.chat/meteor
+## 6.11.0
+
+### Minor Changes
+
+- ([#32498](https://github.com/RocketChat/Rocket.Chat/pull/32498)) Created a `transferChat` Livechat API endpoint for transferring chats programmatically, the endpoint has all the limitations & permissions required that transferring via UI has
+
+- ([#32792](https://github.com/RocketChat/Rocket.Chat/pull/32792)) Allows admins to customize the `Subject` field of Omnichannel email transcripts via setting. By passing a value to the setting `Custom email subject for transcript`, system will use it as the `Subject` field, unless a custom subject is passed when requesting a transcript. If there's no custom subject and setting value is empty, the current default value will be used
+
+- ([#32739](https://github.com/RocketChat/Rocket.Chat/pull/32739)) Fixed an issue where FCM actions did not respect environment's proxy settings
+
+- ([#32706](https://github.com/RocketChat/Rocket.Chat/pull/32706)) Added the possibility for apps to remove users from a room
+
+- ([#32517](https://github.com/RocketChat/Rocket.Chat/pull/32517)) Feature Preview: New Navigation - `Header` and `Contextualbar` size improvements consistent with the new global `NavBar`
+
+- ([#32493](https://github.com/RocketChat/Rocket.Chat/pull/32493)) Fixed Livechat rooms being displayed in the Engagement Dashboard's "Channels" tab
+
+- ([#32742](https://github.com/RocketChat/Rocket.Chat/pull/32742)) Fixed an issue where adding `OVERWRITE_SETTING_` for any setting wasn't immediately taking effect sometimes, and needed a server restart to reflect.
+
+- ([#32752](https://github.com/RocketChat/Rocket.Chat/pull/32752)) Added system messages support for Omnichannel PDF transcripts and email transcripts. Currently these transcripts don't render system messages and is shown as an empty message in PDF/email. This PR adds this support for all valid livechat system messages.
+
+ Also added a new setting under transcripts, to toggle the inclusion of system messages in email and PDF transcripts.
+
+- ([#32793](https://github.com/RocketChat/Rocket.Chat/pull/32793)) New Feature: Video Conference Persistent Chat.
+ This feature provides a discussion id for conference provider apps to store the chat messages exchanged during the conferences, so that those users may then access those messages again at any time through Rocket.Chat.
+- ([#32176](https://github.com/RocketChat/Rocket.Chat/pull/32176)) Added a method to the Apps-Engine that allows apps to read multiple messages from a room
+
+- ([#32493](https://github.com/RocketChat/Rocket.Chat/pull/32493)) Improved Engagement Dashboard's "Channels" tab performance by not returning rooms that had no activity in the analyzed period
+
+- ([#32024](https://github.com/RocketChat/Rocket.Chat/pull/32024)) Implemented a new tab to the users page called 'Active', this tab lists all users who have logged in for the first time and are active.
+
+- ([#32744](https://github.com/RocketChat/Rocket.Chat/pull/32744)) Added account setting `Accounts_Default_User_Preferences_sidebarSectionsOrder` to allow users to reorganize sidebar sections
+
+- ([#32820](https://github.com/RocketChat/Rocket.Chat/pull/32820)) Added a new setting `Livechat_transcript_send_always` that allows admins to decide if email transcript should be sent all the times when a conversation is closed. This setting bypasses agent's preferences. For this setting to work, `Livechat_enable_transcript` should be off, meaning that visitors will no longer receive the option to decide if they want a transcript or not.
+
+- ([#32724](https://github.com/RocketChat/Rocket.Chat/pull/32724)) Extended apps-engine events for users leaving a room to also fire when being removed by another user. Also added the triggering user's information to the event's context payload.
+
+- ([#32777](https://github.com/RocketChat/Rocket.Chat/pull/32777)) Added handling of attachments in Omnichannel email transcripts. Earlier attachments were being skipped and were being shown as empty space, now it should render the image attachments and should show relevant error message for unsupported attachments.
+
+- ([#32800](https://github.com/RocketChat/Rocket.Chat/pull/32800)) Added the ability to filter chats by `queued` on the Current Chats Omnichannel page
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- Bump @rocket.chat/meteor version.
+
+- ([#32679](https://github.com/RocketChat/Rocket.Chat/pull/32679)) Fix validations from "UiKit" modal component
+
+- ([#32730](https://github.com/RocketChat/Rocket.Chat/pull/32730)) Fixed issue in Marketplace that caused a subscription app to show incorrect modals when subscribing
+
+- ([#32628](https://github.com/RocketChat/Rocket.Chat/pull/32628)) Fixed SAML users' full names being updated on login regardless of the "Overwrite user fullname (use idp attribute)" setting
+
+- ([#32692](https://github.com/RocketChat/Rocket.Chat/pull/32692)) Fixed an issue that caused the widget to set the wrong department when using the setDepartment Livechat api endpoint in conjunction with a Livechat Trigger
+
+- ([#32527](https://github.com/RocketChat/Rocket.Chat/pull/32527)) Fixed an inconsistent evaluation of the `Accounts_LoginExpiration` setting over the codebase. In some places, it was being used as milliseconds while in others as days. Invalid values produced different results. A helper function was created to centralize the setting validation and the proper value being returned to avoid edge cases.
+ Negative values may be saved on the settings UI panel but the code will interpret any negative, NaN or 0 value to the default expiration which is 90 days.
+- ([#32626](https://github.com/RocketChat/Rocket.Chat/pull/32626)) livechat `setDepartment` livechat api fixes:
+ - Changing department didn't reflect on the registration form in real time
+ - Changing the department mid conversation didn't transfer the chat
+ - Depending on the state of the department, it couldn't be set as default
+- ([#32810](https://github.com/RocketChat/Rocket.Chat/pull/32810)) Fixed issue where bad word filtering was not working in the UI for messages
+
+- ([#32707](https://github.com/RocketChat/Rocket.Chat/pull/32707)) Fixed issue with livechat agents not being able to leave omnichannel rooms if joining after a room has been closed by the visitor (due to race conditions)
+
+- ([#32837](https://github.com/RocketChat/Rocket.Chat/pull/32837)) Fixed an issue where non-encrypted attachments were not being downloaded
+
+- ([#32861](https://github.com/RocketChat/Rocket.Chat/pull/32861)) fixed the contextual bar closing when editing thread messages instead of cancelling the message edit
+
+- ([#32713](https://github.com/RocketChat/Rocket.Chat/pull/32713)) Fixed the disappearance of some settings after navigation under network latency.
+
+- ([#32592](https://github.com/RocketChat/Rocket.Chat/pull/32592)) Fixes Missing line breaks on Omnichannel Room Info Panel
+
+- ([#32807](https://github.com/RocketChat/Rocket.Chat/pull/32807)) Fixed web client crashing on Firefox private window. Firefox disables access to service workers inside private windows. Rocket.Chat needs service workers to process E2EE encrypted files on rooms. These types of files won't be available inside private windows, but the rest of E2EE encrypted features should work normally
+
+- ([#32864](https://github.com/RocketChat/Rocket.Chat/pull/32864)) fixed an issue in the "Create discussion" form, that would have the "Create" action button disabled even though the form is prefilled when opening it from the message action
+
+- ([#32691](https://github.com/RocketChat/Rocket.Chat/pull/32691)) Removed 'Hide' option in the room menu for Omnichannel conversations.
+
+- ([#32445](https://github.com/RocketChat/Rocket.Chat/pull/32445)) Fixed LDAP rooms, teams and roles syncs not being triggered on login even when the "Update User Data on Login" setting is enabled
+
+- ([#32328](https://github.com/RocketChat/Rocket.Chat/pull/32328)) Allow customFields on livechat creation bridge
+
+- ([#32803](https://github.com/RocketChat/Rocket.Chat/pull/32803)) Fixed "Copy link" message action enabled in Starred and Pinned list for End to End Encrypted channels, this action is disabled now
+
+- ([#32769](https://github.com/RocketChat/Rocket.Chat/pull/32769)) Fixed issue that caused unintentional clicks when scrolling the channels sidebar on safari/chrome in iOS
+
+- ([#32857](https://github.com/RocketChat/Rocket.Chat/pull/32857)) Fixed some anomalies related to disabled E2EE rooms. Earlier there are some weird issues with disabled E2EE rooms, this PR fixes these anomalies.
+
+- ([#32765](https://github.com/RocketChat/Rocket.Chat/pull/32765)) Fixed an issue that prevented the option to start a discussion from being shown on the message actions
+
+- ([#32671](https://github.com/RocketChat/Rocket.Chat/pull/32671)) Fix show correct user roles after updating user roles on admin edit user panel.
+
+- ([#32482](https://github.com/RocketChat/Rocket.Chat/pull/32482)) Fixed an issue with blocked login when dismissed 2FA modal by clicking outside of it or pressing the escape key
+
+- ([#32804](https://github.com/RocketChat/Rocket.Chat/pull/32804)) Fixes an issue not displaying all groups in settings list
+
+- ([#32815](https://github.com/RocketChat/Rocket.Chat/pull/32815)) Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)
+
+- ([#32632](https://github.com/RocketChat/Rocket.Chat/pull/32632)) Improving UX by change the position of room info actions buttons and menu order to avoid missclick in destructive actions.
+
+- ([#32752](https://github.com/RocketChat/Rocket.Chat/pull/32752)) Reduced time on generation of PDF transcripts. Earlier Rocket.Chat was fetching the required translations everytime a PDF transcript was requested, this process was async and was being unnecessarily being performed on every pdf transcript request. This PR improves this and now the translations are loaded at the start and kept in memory to process further pdf transcripts requests. This reduces the time of asynchronously fetching translations again and again.
+
+- ([#32719](https://github.com/RocketChat/Rocket.Chat/pull/32719)) Added the `user` param to apps-engine update method call, allowing apps' new `onUpdate` hook to know who triggered the update.
+
+- Updated dependencies [88e5219bd2, b4bbcbfc9a, 8fc6ca8b4e, 25da5280a5, 1b7b1161cf, 439faa87d3, 03c8b066f9, 2d89a0c448, 439faa87d3, 24f7df4894, 3ffe4a2944, 3b4b19cfc5, 4e8aa575a6, 03c8b066f9, 264d7d5496, b8e5887fb9]:
+
+ - @rocket.chat/fuselage-ui-kit@9.0.0
+ - @rocket.chat/i18n@0.6.0
+ - @rocket.chat/tools@0.2.2
+ - @rocket.chat/ui-client@9.0.0
+ - @rocket.chat/model-typings@0.6.0
+ - @rocket.chat/omnichannel-services@0.3.0
+ - @rocket.chat/pdf-worker@0.2.0
+ - @rocket.chat/core-services@0.5.0
+ - @rocket.chat/ui-video-conf@9.0.0
+ - @rocket.chat/core-typings@6.11.0
+ - @rocket.chat/ui-contexts@9.0.0
+ - @rocket.chat/models@0.2.0
+ - @rocket.chat/ui-kit@0.36.0
+ - @rocket.chat/web-ui-registration@9.0.0
+ - @rocket.chat/rest-typings@6.11.0
+ - @rocket.chat/apps@0.1.3
+ - @rocket.chat/presence@0.2.3
+ - @rocket.chat/gazzodown@9.0.0
+ - @rocket.chat/api-client@0.2.3
+ - @rocket.chat/license@0.2.3
+ - @rocket.chat/cron@0.1.3
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0
+ - @rocket.chat/instance-status@0.1.3
+ - @rocket.chat/server-cloud-communication@0.0.2
+
+
+## 6.11.0-rc.6
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.6
+ - @rocket.chat/rest-typings@6.11.0-rc.6
+ - @rocket.chat/api-client@0.2.3-rc.6
+ - @rocket.chat/license@0.2.3-rc.6
+ - @rocket.chat/omnichannel-services@0.3.0-rc.6
+ - @rocket.chat/pdf-worker@0.2.0-rc.6
+ - @rocket.chat/presence@0.2.3-rc.6
+ - @rocket.chat/apps@0.1.3-rc.6
+ - @rocket.chat/core-services@0.5.0-rc.6
+ - @rocket.chat/cron@0.1.3-rc.6
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.6
+ - @rocket.chat/gazzodown@9.0.0-rc.6
+ - @rocket.chat/model-typings@0.6.0-rc.6
+ - @rocket.chat/ui-contexts@9.0.0-rc.6
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.6
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.6
+ - @rocket.chat/ui-client@9.0.0-rc.6
+ - @rocket.chat/ui-video-conf@9.0.0-rc.6
+ - @rocket.chat/web-ui-registration@9.0.0-rc.6
+ - @rocket.chat/instance-status@0.1.3-rc.6
+
+
+## 6.11.0-rc.5
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.5
+ - @rocket.chat/rest-typings@6.11.0-rc.5
+ - @rocket.chat/api-client@0.2.3-rc.5
+ - @rocket.chat/license@0.2.3-rc.5
+ - @rocket.chat/omnichannel-services@0.3.0-rc.5
+ - @rocket.chat/pdf-worker@0.2.0-rc.5
+ - @rocket.chat/presence@0.2.3-rc.5
+ - @rocket.chat/apps@0.1.3-rc.5
+ - @rocket.chat/core-services@0.5.0-rc.5
+ - @rocket.chat/cron@0.1.3-rc.5
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.5
+ - @rocket.chat/gazzodown@9.0.0-rc.5
+ - @rocket.chat/model-typings@0.6.0-rc.5
+ - @rocket.chat/ui-contexts@9.0.0-rc.5
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.5
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.5
+ - @rocket.chat/ui-client@9.0.0-rc.5
+ - @rocket.chat/ui-video-conf@9.0.0-rc.5
+ - @rocket.chat/web-ui-registration@9.0.0-rc.5
+ - @rocket.chat/instance-status@0.1.3-rc.5
+
+
+## 6.11.0-rc.4
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.4
+ - @rocket.chat/rest-typings@6.11.0-rc.4
+ - @rocket.chat/api-client@0.2.3-rc.4
+ - @rocket.chat/license@0.2.3-rc.4
+ - @rocket.chat/omnichannel-services@0.3.0-rc.4
+ - @rocket.chat/pdf-worker@0.2.0-rc.4
+ - @rocket.chat/presence@0.2.3-rc.4
+ - @rocket.chat/apps@0.1.3-rc.4
+ - @rocket.chat/core-services@0.5.0-rc.4
+ - @rocket.chat/cron@0.1.3-rc.4
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.4
+ - @rocket.chat/gazzodown@9.0.0-rc.4
+ - @rocket.chat/model-typings@0.6.0-rc.4
+ - @rocket.chat/ui-contexts@9.0.0-rc.4
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.4
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.4
+ - @rocket.chat/ui-client@9.0.0-rc.4
+ - @rocket.chat/ui-video-conf@9.0.0-rc.4
+ - @rocket.chat/web-ui-registration@9.0.0-rc.4
+ - @rocket.chat/instance-status@0.1.3-rc.4
+
+
+## 6.11.0-rc.3
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.3
+ - @rocket.chat/rest-typings@6.11.0-rc.3
+ - @rocket.chat/api-client@0.2.3-rc.3
+ - @rocket.chat/license@0.2.3-rc.3
+ - @rocket.chat/omnichannel-services@0.3.0-rc.3
+ - @rocket.chat/pdf-worker@0.2.0-rc.3
+ - @rocket.chat/presence@0.2.3-rc.3
+ - @rocket.chat/apps@0.1.3-rc.3
+ - @rocket.chat/core-services@0.5.0-rc.3
+ - @rocket.chat/cron@0.1.3-rc.3
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.3
+ - @rocket.chat/gazzodown@9.0.0-rc.3
+ - @rocket.chat/model-typings@0.6.0-rc.3
+ - @rocket.chat/ui-contexts@9.0.0-rc.3
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.3
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.3
+ - @rocket.chat/ui-client@9.0.0-rc.3
+ - @rocket.chat/ui-video-conf@9.0.0-rc.3
+ - @rocket.chat/web-ui-registration@9.0.0-rc.3
+ - @rocket.chat/instance-status@0.1.3-rc.3
+
+
## 6.11.0-rc.2
### Patch Changes
diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts
index a61149c5e66e..9cbf202896e1 100644
--- a/apps/meteor/app/api/server/v1/emoji-custom.ts
+++ b/apps/meteor/app/api/server/v1/emoji-custom.ts
@@ -3,6 +3,8 @@ import { EmojiCustom } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { SystemLogger } from '../../../../server/lib/logger/system';
+import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji';
+import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom';
import { settings } from '../../../settings/server';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
@@ -148,9 +150,19 @@ API.v1.addRoute(
fields.extension = emojiToUpdate.extension;
}
- await Meteor.callAsync('insertOrUpdateEmoji', { ...fields, newFile });
+ const emojiData = {
+ name: fields.name,
+ _id: fields._id,
+ aliases: fields.aliases,
+ extension: fields.extension,
+ previousName: fields.previousName,
+ previousExtension: fields.previousExtension,
+ newFile,
+ };
+
+ await insertOrUpdateEmoji(this.userId, emojiData);
if (fields.newFile) {
- await Meteor.callAsync('uploadEmojiCustom', fileBuffer, mimetype, { ...fields, newFile });
+ await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData);
}
return API.v1.success();
},
diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts
index 7be5b1fc13fe..5f998546cf3e 100644
--- a/apps/meteor/app/api/server/v1/federation.ts
+++ b/apps/meteor/app/api/server/v1/federation.ts
@@ -22,3 +22,21 @@ API.v1.addRoute(
},
},
);
+
+API.v1.addRoute(
+ 'federation/configuration.verify',
+ { authRequired: true, permissionsRequired: ['view-privileged-setting'] },
+ {
+ async get() {
+ const service = License.hasValidLicense() ? FederationEE : Federation;
+
+ const status = await service.configurationStatus();
+
+ if (!status.externalReachability.ok || !status.appservice.ok) {
+ return API.v1.failure(status);
+ }
+
+ return API.v1.success(status);
+ },
+ },
+);
diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts
index 034a73f54104..4113b945a4db 100644
--- a/apps/meteor/app/api/server/v1/oauthapps.ts
+++ b/apps/meteor/app/api/server/v1/oauthapps.ts
@@ -27,11 +27,12 @@ API.v1.addRoute(
{ authRequired: true, validateParams: isOauthAppsGetParams },
{
async get() {
- if (!(await hasPermissionAsync(this.userId, 'manage-oauth-apps'))) {
- return API.v1.unauthorized();
- }
+ const isOAuthAppsManager = await hasPermissionAsync(this.userId, 'manage-oauth-apps');
- const oauthApp = await OAuthApps.findOneAuthAppByIdOrClientId(this.queryParams);
+ const oauthApp = await OAuthApps.findOneAuthAppByIdOrClientId(
+ this.queryParams,
+ !isOAuthAppsManager ? { projection: { clientSecret: 0 } } : {},
+ );
if (!oauthApp) {
return API.v1.failure('OAuth app not found.');
diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts
index e3296b98ef17..17ef75b74574 100644
--- a/apps/meteor/app/api/server/v1/rooms.ts
+++ b/apps/meteor/app/api/server/v1/rooms.ts
@@ -1,8 +1,14 @@
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 { Messages, Rooms, Users, Uploads, Subscriptions } from '@rocket.chat/models';
import type { Notifications } from '@rocket.chat/rest-typings';
-import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps, isRoomsExportProps } from '@rocket.chat/rest-typings';
+import {
+ isGETRoomsNameExists,
+ isRoomsImagesProps,
+ isRoomsMuteUnmuteUserProps,
+ isRoomsExportProps,
+ isRoomsIsMemberProps,
+} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';
import { isTruthy } from '../../../../lib/isTruthy';
@@ -783,6 +789,36 @@ API.v1.addRoute(
},
);
+API.v1.addRoute(
+ 'rooms.isMember',
+ {
+ authRequired: true,
+ validateParams: isRoomsIsMemberProps,
+ },
+ {
+ async get() {
+ const { roomId, userId, username } = this.queryParams;
+ const [room, user] = await Promise.all([
+ findRoomByIdOrName({
+ params: { roomId },
+ }) as Promise,
+ Users.findOneByIdOrUsername(userId || username),
+ ]);
+
+ if (!user?._id) {
+ return API.v1.failure('error-user-not-found');
+ }
+
+ if (await canAccessRoomAsync(room, { _id: this.user._id })) {
+ return API.v1.success({
+ isMember: (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) > 0,
+ });
+ }
+ return API.v1.unauthorized();
+ },
+ },
+);
+
API.v1.addRoute(
'rooms.muteUser',
{ authRequired: true, validateParams: isRoomsMuteUnmuteUserProps },
diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts
index 9d81fe6bef65..b92d9ba572fd 100644
--- a/apps/meteor/app/api/server/v1/subscriptions.ts
+++ b/apps/meteor/app/api/server/v1/subscriptions.ts
@@ -82,6 +82,7 @@ API.v1.addRoute(
async post() {
const { readThreads = false } = this.bodyParams;
const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId;
+
await readMessages(roomId, this.userId, readThreads);
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 7ae585b89dfa..9c56ecac01cb 100644
--- a/apps/meteor/app/api/server/v1/users.ts
+++ b/apps/meteor/app/api/server/v1/users.ts
@@ -45,6 +45,7 @@ import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar';
import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername';
import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields';
import { validateNameChars } from '../../../lib/server/functions/validateNameChars';
+import { validateUsername } from '../../../lib/server/functions/validateUsername';
import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener';
import { generateAccessToken } from '../../../lib/server/methods/createToken';
import { settings } from '../../../settings/server';
@@ -651,6 +652,10 @@ API.v1.addRoute(
return API.v1.failure('Name contains invalid characters');
}
+ if (!validateUsername(this.bodyParams.username)) {
+ return API.v1.failure(`The username provided is not valid`);
+ }
+
if (!(await checkUsernameAvailability(this.bodyParams.username))) {
return API.v1.failure('Username is already in use');
}
diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts
index ec5cff29a99b..4f4794591e02 100644
--- a/apps/meteor/app/apps/server/bridges/livechat.ts
+++ b/apps/meteor/app/apps/server/bridges/livechat.ts
@@ -1,7 +1,7 @@
-import type { IAppServerOrchestrator, IAppsLivechatMessage } from '@rocket.chat/apps';
+import type { IAppServerOrchestrator, IAppsLivechatMessage, IAppsMessage } from '@rocket.chat/apps';
import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator';
import type { IVisitor, ILivechatRoom, ILivechatTransferData, IDepartment } from '@rocket.chat/apps-engine/definition/livechat';
-import type { IMessage as IAppsEngineMesage } from '@rocket.chat/apps-engine/definition/messages';
+import type { IMessage as IAppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge';
import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings';
@@ -13,6 +13,12 @@ import { deasyncPromise } from '../../../../server/deasync/deasync';
import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped';
import { settings } from '../../../settings/server';
+declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' {
+ export interface IAppMessagesConverter {
+ convertMessage(message: IMessage, cacheObj?: object): Promise;
+ }
+}
+
declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' {
interface IExtraRoomParams {
customFields?: Record;
@@ -337,7 +343,7 @@ export class AppLivechatBridge extends LivechatBridge {
return Promise.all((await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter));
}
- protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> {
+ protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> {
this.orch.debugLog(`The App ${appId} is getting the transcript for livechat room ${roomId}.`);
const messageConverter = this.orch.getConverters()?.get('messages');
@@ -346,8 +352,7 @@ export class AppLivechatBridge extends LivechatBridge {
}
const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId });
-
- return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise));
+ return Promise.all(await livechatMessages.map((message) => messageConverter.convertMessage(message, livechatMessages)).toArray());
}
protected async setCustomFields(
diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts
index 18a68220998f..824a9d5c15af 100644
--- a/apps/meteor/app/apps/server/bridges/messages.ts
+++ b/apps/meteor/app/apps/server/bridges/messages.ts
@@ -1,4 +1,5 @@
import type { IAppServerOrchestrator, IAppsMessage, IAppsUser } from '@rocket.chat/apps';
+import type { Reaction } from '@rocket.chat/apps-engine/definition/messages';
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge';
import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge';
@@ -10,6 +11,7 @@ import { deleteMessage } from '../../../lib/server/functions/deleteMessage';
import { updateMessage } from '../../../lib/server/functions/updateMessage';
import { executeSendMessage } from '../../../lib/server/methods/sendMessage';
import notifications from '../../../notifications/server/lib/Notifications';
+import { executeSetReaction } from '../../../reactions/server/setReaction';
export class AppMessageBridge extends MessageBridge {
constructor(private readonly orch: IAppServerOrchestrator) {
@@ -118,4 +120,24 @@ export class AppMessageBridge extends MessageBridge {
throw new Error('Unrecognized typing scope provided');
}
}
+
+ private isValidReaction(reaction: Reaction): boolean {
+ return reaction.startsWith(':') && reaction.endsWith(':');
+ }
+
+ protected async addReaction(messageId: string, userId: string, reaction: Reaction): Promise {
+ if (!this.isValidReaction(reaction)) {
+ throw new Error('Invalid reaction');
+ }
+
+ return executeSetReaction(userId, reaction, messageId, true);
+ }
+
+ protected async removeReaction(messageId: string, userId: string, reaction: Reaction): Promise {
+ if (!this.isValidReaction(reaction)) {
+ throw new Error('Invalid reaction');
+ }
+
+ return executeSetReaction(userId, reaction, messageId, false);
+ }
}
diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js
index d7dae512e9a8..89ef2454d895 100644
--- a/apps/meteor/app/apps/server/converters/messages.js
+++ b/apps/meteor/app/apps/server/converters/messages.js
@@ -52,19 +52,26 @@ export class AppMessagesConverter {
return transformMappedData(message, map);
}
- async convertMessage(msgObj) {
+ async convertMessage(msgObj, cacheObj = msgObj) {
if (!msgObj) {
return undefined;
}
const cache =
- this.mem.get(msgObj) ??
+ this.mem.get(cacheObj) ??
new Map([
['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))],
- ['user', cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users')))],
+ [
+ 'user.convertById',
+ cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users'))),
+ ],
+ [
+ 'user.convertToApp',
+ cachedFunction(this.orch.getConverters().get('users').convertToApp.bind(this.orch.getConverters().get('users'))),
+ ],
]);
- this.mem.set(msgObj, cache);
+ this.mem.set(cacheObj, cache);
const map = {
id: '_id',
@@ -96,7 +103,7 @@ export class AppMessagesConverter {
return undefined;
}
- return cache.get('user')(editedBy._id);
+ return cache.get('user.convertById')(editedBy._id);
},
attachments: async (message) => {
const result = await this._convertAttachmentsToApp(message.attachments);
@@ -110,8 +117,8 @@ export class AppMessagesConverter {
// When the message contains token, means the message is from the visitor(omnichannel)
const user = await (isMessageFromVisitor(msgObj)
- ? this.orch.getConverters().get('users').convertToApp(message.u)
- : cache.get('user')(message.u._id));
+ ? cache.get('user.convertToApp')(message.u)
+ : cache.get('user.convertById')(message.u._id));
delete message.u;
@@ -120,7 +127,7 @@ export class AppMessagesConverter {
* `sender` as undefined, so we need to add this fallback here.
*/
- return user || this.orch.getConverters().get('users').convertToApp(message.u);
+ return user || cache.get('user.convertToApp')(message.u);
},
};
diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js
index 670c1a248a0f..a98a6701b2c2 100644
--- a/apps/meteor/app/apps/server/converters/rooms.js
+++ b/apps/meteor/app/apps/server/converters/rooms.js
@@ -111,8 +111,8 @@ export class AppRoomsConverter {
return Object.assign(newRoom, room._unmappedProperties_);
}
- async convertRoom(room) {
- if (!room) {
+ async convertRoom(originalRoom) {
+ if (!originalRoom) {
return undefined;
}
@@ -134,6 +134,7 @@ export class AppRoomsConverter {
_USERNAMES: '_USERNAMES',
description: 'description',
source: 'source',
+ closer: 'closer',
isDefault: (room) => {
const result = !!room.default;
delete room.default;
@@ -210,6 +211,19 @@ export class AppRoomsConverter {
return this.orch.getConverters().get('departments').convertById(departmentId);
},
+ closedBy: async (room) => {
+ const { closedBy } = room;
+
+ if (!closedBy) {
+ return undefined;
+ }
+
+ delete room.closedBy;
+ if (originalRoom.closer === 'user') {
+ return this.orch.getConverters().get('users').convertById(closedBy._id);
+ }
+ return this.orch.getConverters().get('visitors').convertById(closedBy._id);
+ },
servedBy: async (room) => {
const { servedBy } = room;
@@ -245,7 +259,7 @@ export class AppRoomsConverter {
},
};
- return transformMappedData(room, map);
+ return transformMappedData(originalRoom, map);
}
_convertTypeToApp(typeChar) {
diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts
index 6efe99e14d0e..d9ae4133e49e 100644
--- a/apps/meteor/app/authorization/server/constant/permissions.ts
+++ b/apps/meteor/app/authorization/server/constant/permissions.ts
@@ -93,6 +93,10 @@ export const permissions = [
_id: 'view-l-room',
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
},
+ {
+ _id: 'create-livechat-contact',
+ roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
+ },
{ _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
{
_id: 'view-omnichannel-contact-center',
diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts
index 1e6c224c4115..f3c6d9e55fdb 100644
--- a/apps/meteor/app/autotranslate/server/autotranslate.ts
+++ b/apps/meteor/app/autotranslate/server/autotranslate.ts
@@ -79,7 +79,7 @@ export class TranslationProviderRegistry {
return null;
}
- return provider.translateMessage(message, room, targetLanguage);
+ return provider.translateMessage(message, { room, targetLanguage });
}
static getProviders(): AutoTranslate[] {
@@ -290,7 +290,7 @@ export abstract class AutoTranslate {
* @param {object} targetLanguage
* @returns {object} unmodified message object.
*/
- async translateMessage(message: IMessage, room: IRoom, targetLanguage?: string): Promise {
+ async translateMessage(message: IMessage, { room, targetLanguage }: { room: IRoom; targetLanguage?: string }): Promise {
let targetLanguages: string[];
if (targetLanguage) {
targetLanguages = [targetLanguage];
diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts
index 2f119c948263..3d4d15c0316e 100644
--- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts
+++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts
@@ -4,6 +4,7 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
+import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -44,6 +45,8 @@ Meteor.methods({
});
}
+ let shouldNotifySubscriptionChanged = false;
+
switch (field) {
case 'autoTranslate':
const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } });
@@ -53,16 +56,34 @@ Meteor.methods({
});
}
- await Subscriptions.updateAutoTranslateById(subscription._id, value === '1');
+ const updateAutoTranslateResponse = await Subscriptions.updateAutoTranslateById(subscription._id, value === '1');
+ if (updateAutoTranslateResponse.modifiedCount) {
+ shouldNotifySubscriptionChanged = true;
+ }
+
if (!subscription.autoTranslateLanguage && options.defaultLanguage) {
- await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage);
+ const updateAutoTranslateLanguageResponse = await Subscriptions.updateAutoTranslateLanguageById(
+ subscription._id,
+ options.defaultLanguage,
+ );
+ if (updateAutoTranslateLanguageResponse.modifiedCount) {
+ shouldNotifySubscriptionChanged = true;
+ }
}
+
break;
case 'autoTranslateLanguage':
- await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value);
+ const updateAutoTranslateLanguage = await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value);
+ if (updateAutoTranslateLanguage.modifiedCount) {
+ shouldNotifySubscriptionChanged = true;
+ }
break;
}
+ if (shouldNotifySubscriptionChanged) {
+ void notifyOnSubscriptionChangedById(subscription._id);
+ }
+
return true;
},
});
diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts
index 55d40cf3d7e6..ef70ff65c067 100644
--- a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts
+++ b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts
@@ -3,21 +3,28 @@ import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { UpdateResult } from 'mongodb';
+import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener';
+
export const saveRoomCustomFields = async function (rid: string, roomCustomFields: Record): Promise {
if (!Match.test(rid, String)) {
throw new Meteor.Error('invalid-room', 'Invalid room', {
function: 'RocketChat.saveRoomCustomFields',
});
}
+
if (!Match.test(roomCustomFields, Object)) {
throw new Meteor.Error('invalid-roomCustomFields-type', 'Invalid roomCustomFields type', {
function: 'RocketChat.saveRoomCustomFields',
});
}
+
const ret = await Rooms.setCustomFieldsById(rid, roomCustomFields);
// Update customFields of any user's Subscription related with this rid
- await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields);
+ const { modifiedCount } = await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields);
+ if (modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
return ret;
};
diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts
index ed07540ba2b0..c1a441463a98 100644
--- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts
+++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts
@@ -6,6 +6,8 @@ import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { UpdateResult } from 'mongodb';
+import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener';
+
export const saveRoomEncrypted = async function (rid: string, encrypted: boolean, user: IUser, sendMessage = true): Promise {
if (!Match.test(rid, String)) {
throw new Meteor.Error('invalid-room', 'Invalid room', {
@@ -27,7 +29,10 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean
}
if (encrypted) {
- await Subscriptions.disableAutoTranslateByRoomId(rid);
+ const { modifiedCount } = await Subscriptions.disableAutoTranslateByRoomId(rid);
+ if (modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
}
return update;
};
diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts
index 0fc15f878bcf..f4a5afbb6380 100644
--- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts
+++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts
@@ -1,4 +1,4 @@
-import { Message } from '@rocket.chat/core-services';
+import { Message, Room } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import { Integrations, Rooms, Subscriptions } from '@rocket.chat/models';
@@ -8,11 +8,17 @@ 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 { notifyOnIntegrationChangedByChannels, notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener';
import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName';
const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => {
- return Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]);
+ const responses = await Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]);
+
+ if (responses[1]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
+ return responses;
};
const updateRoomName = async (rid: string, displayName: string, slugifiedRoomName: string) => {
@@ -24,10 +30,16 @@ const updateRoomName = async (rid: string, displayName: string, slugifiedRoomNam
});
}
- return Promise.all([
+ const responses = await Promise.all([
Rooms.setNameById(rid, slugifiedRoomName, displayName),
Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName),
]);
+
+ if (responses[1]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
+ return responses;
};
export async function saveRoomName(
@@ -48,6 +60,9 @@ export async function saveRoomName(
function: 'RocketChat.saveRoomdisplayName',
});
}
+
+ await Room.beforeNameChange(room);
+
if (displayName === room.name) {
return;
}
diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts
index 11b9b5b6e565..a59f2ba82fba 100644
--- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts
+++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts
@@ -1,4 +1,4 @@
-import { Message } from '@rocket.chat/core-services';
+import { Message, Room } from '@rocket.chat/core-services';
import { Rooms } from '@rocket.chat/models';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -20,6 +20,10 @@ export const saveRoomTopic = async function (
});
}
+ const room = await Rooms.findOneById(rid);
+
+ await Room.beforeTopicChange(room!);
+
const update = await Rooms.setTopicById(rid, roomTopic);
if (update && sendMessage) {
await Message.saveSystemMessage('room_changed_topic', rid, roomTopic || '', user);
diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts
index e8a60d1ea0eb..4600d1d46a80 100644
--- a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts
+++ b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts
@@ -8,6 +8,7 @@ import type { UpdateResult, Document } from 'mongodb';
import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig';
import { i18n } from '../../../../server/lib/i18n';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
+import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
export const saveRoomType = async function (
@@ -41,11 +42,16 @@ export const saveRoomType = async function (
});
}
- const result = (await Rooms.setTypeById(rid, roomType)) && (await Subscriptions.updateTypeByRoomId(rid, roomType));
+ const result = await Promise.all([Rooms.setTypeById(rid, roomType), Subscriptions.updateTypeByRoomId(rid, roomType)]);
+
if (!result) {
return result;
}
+ if (result[1]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
if (sendMessage) {
let message;
if (roomType === 'c') {
@@ -59,5 +65,6 @@ export const saveRoomType = async function (
}
await Message.saveSystemMessage('room_changed_privacy', rid, message, user);
}
+
return result;
};
diff --git a/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.ts b/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.ts
deleted file mode 100644
index aee596402cc6..000000000000
--- a/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Rooms } from '@rocket.chat/models';
-import { Match, check } from 'meteor/check';
-import { Meteor } from 'meteor/meteor';
-
-export const saveStreamingOptions = async function (rid: string, options: Record): Promise {
- if (!Match.test(rid, String)) {
- throw new Meteor.Error('invalid-room', 'Invalid room', {
- function: 'RocketChat.saveStreamingOptions',
- });
- }
-
- check(options, {
- id: Match.Optional(String),
- type: Match.Optional(String),
- url: Match.Optional(String),
- thumbnail: Match.Optional(String),
- isAudioOnly: Match.Optional(Boolean),
- message: Match.Optional(String),
- });
-
- await Rooms.setStreamingOptionsById(rid, options);
-};
diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts
index 44ad253d83ef..04e8fdbaf186 100644
--- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts
+++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts
@@ -21,7 +21,6 @@ import { saveRoomReadOnly } from '../functions/saveRoomReadOnly';
import { saveRoomSystemMessages } from '../functions/saveRoomSystemMessages';
import { saveRoomTopic } from '../functions/saveRoomTopic';
import { saveRoomType } from '../functions/saveRoomType';
-import { saveStreamingOptions } from '../functions/saveStreamingOptions';
type RoomSettings = {
roomAvatar: string;
@@ -37,7 +36,6 @@ type RoomSettings = {
systemMessages: MessageTypesValues[];
default: boolean;
joinCode: string;
- streamingOptions: NonNullable;
retentionEnabled: boolean;
retentionMaxAge: number;
retentionExcludePinned: boolean;
@@ -272,9 +270,6 @@ const settingSavers: RoomSettingsSavers = {
void Team.update(user._id, room.teamId, { type, updateRoom: false });
}
},
- async streamingOptions({ value, rid }) {
- await saveStreamingOptions(rid, value);
- },
async readOnly({ value, room, rid, user }) {
if (value !== room.ro) {
await saveRoomReadOnly(rid, value, user);
@@ -354,7 +349,6 @@ const fields: (keyof RoomSettings)[] = [
'systemMessages',
'default',
'joinCode',
- 'streamingOptions',
'retentionEnabled',
'retentionMaxAge',
'retentionExcludePinned',
diff --git a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
index 05cf2326156f..1ff9ed1dc1ba 100644
--- a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
+++ b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
@@ -22,7 +22,7 @@ const updateAndNotifyParentRoomWithParentMessage = async (room: IRoom): Promise<
*/
callbacks.add(
'afterSaveMessage',
- async (message, { _id, prid }) => {
+ async (message, { room: { _id, prid } }) => {
if (!prid) {
return message;
}
diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts
index 6e670d723ec9..96e0bd846390 100644
--- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts
+++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts
@@ -5,7 +5,6 @@ import { Messages, Rooms, Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import { Meteor } from 'meteor/meteor';
-import { callbacks } from '../../../../lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage';
@@ -14,6 +13,7 @@ import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
import { attachMessage } from '../../../lib/server/functions/attachMessage';
import { createRoom } from '../../../lib/server/functions/createRoom';
import { sendMessage } from '../../../lib/server/functions/sendMessage';
+import { afterSaveMessageAsync } from '../../../lib/server/lib/afterSaveMessage';
import { settings } from '../../../settings/server';
const getParentRoom = async (rid: IRoom['_id']) => {
@@ -27,13 +27,11 @@ async function createDiscussionMessage(
drid: IRoom['_id'],
msg: IMessage['msg'],
messageEmbedded?: MessageAttachmentDefault,
-): Promise {
- const msgId = await Message.saveSystemMessage('discussion-created', rid, msg, user, {
+): Promise {
+ return Message.saveSystemMessage('discussion-created', rid, msg, user, {
drid,
...(messageEmbedded && { attachments: [messageEmbedded] }),
});
-
- return Messages.findOneById(msgId);
}
async function mentionMessage(
@@ -191,8 +189,9 @@ const create = async ({
}
if (discussionMsg) {
- callbacks.runAsync('afterSaveMessage', discussionMsg, parentRoom);
+ afterSaveMessageAsync(discussionMsg, parentRoom);
}
+
return discussion;
};
diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts
index 860051c04d4d..22eccf03f407 100644
--- a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts
+++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts
@@ -1,6 +1,8 @@
import { Rooms, Subscriptions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
+import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener';
+
export async function handleSuggestedGroupKey(
handle: 'accept' | 'reject',
rid: string,
@@ -30,5 +32,8 @@ export async function handleSuggestedGroupKey(
await Rooms.addUserIdToE2EEQueueByRoomIds([sub.rid], userId);
}
- await Subscriptions.unsetGroupE2ESuggestedKey(sub._id);
+ const { modifiedCount } = await Subscriptions.unsetGroupE2ESuggestedKey(sub._id);
+ if (modifiedCount) {
+ void notifyOnSubscriptionChangedById(sub._id);
+ }
}
diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts
index 5764a021f54c..87182f723e7d 100644
--- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts
+++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts
@@ -3,6 +3,7 @@ import { Subscriptions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
+import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../lib/server/lib/notifyListener';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -25,12 +26,18 @@ Meteor.methods({
if (mySub) {
// Setting the key to myself, can set directly to the final field
if (userId === uid) {
- await Subscriptions.setGroupE2EKey(mySub._id, key);
+ const setGroupE2EKeyResponse = await Subscriptions.setGroupE2EKey(mySub._id, key);
+ if (setGroupE2EKeyResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedById(mySub._id);
+ }
return;
}
// uid also has subscription to this room
- await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key);
+ const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key);
+ if (modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid);
+ }
}
},
});
diff --git a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts
new file mode 100644
index 000000000000..7e838baee9b0
--- /dev/null
+++ b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts
@@ -0,0 +1,148 @@
+import { api } from '@rocket.chat/core-services';
+import { EmojiCustom } from '@rocket.chat/models';
+import limax from 'limax';
+import { Meteor } from 'meteor/meteor';
+import _ from 'underscore';
+
+import { trim } from '../../../../lib/utils/stringUtils';
+import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
+import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
+
+type EmojiData = {
+ _id?: string;
+ name: string;
+ aliases: string;
+ extension: string;
+ previousName?: string;
+ previousExtension?: string;
+ newFile?: boolean;
+};
+
+type EmojiDataWithParsedAliases = Omit & { _id: string; aliases: string[] };
+
+export async function insertOrUpdateEmoji(userId: string | null, emojiData: EmojiData): Promise {
+ if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (!trim(emojiData.name)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', {
+ method: 'insertOrUpdateEmoji',
+ field: 'Name',
+ });
+ }
+
+ emojiData.name = limax(emojiData.name, { replacement: '_' });
+ emojiData.aliases = limax(emojiData.aliases, { replacement: '_' });
+
+ // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, )
+ // more practical than allowing specific sets of characters; also allows foreign languages
+ const nameValidation = /[\s,:><&"'\/\\\(\)]/;
+ const aliasValidation = /[:><&\|"'\/\\\(\)]/;
+
+ // silently strip colon; this allows for uploading :emojiname: as emojiname
+ emojiData.name = emojiData.name.replace(/:/g, '');
+ emojiData.aliases = emojiData.aliases.replace(/:/g, '');
+
+ if (nameValidation.test(emojiData.name)) {
+ throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.name} is not a valid name`, {
+ method: 'insertOrUpdateEmoji',
+ input: emojiData.name,
+ field: 'Name',
+ });
+ }
+
+ let aliases: string[] = [];
+ if (emojiData.aliases) {
+ if (aliasValidation.test(emojiData.aliases)) {
+ throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.aliases} is not a valid alias set`, {
+ method: 'insertOrUpdateEmoji',
+ input: emojiData.aliases,
+ field: 'Alias_Set',
+ });
+ }
+ aliases = _.without(emojiData.aliases.split(/[\s,]/).filter(Boolean), emojiData.name);
+ }
+
+ emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
+
+ let matchingResults = [];
+
+ if (emojiData._id) {
+ matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray();
+ for await (const alias of aliases) {
+ matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray());
+ }
+ } else {
+ matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray();
+ for await (const alias of aliases) {
+ matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray());
+ }
+ }
+
+ if (matchingResults.length > 0) {
+ throw new Meteor.Error('Custom_Emoji_Error_Name_Or_Alias_Already_In_Use', 'The custom emoji or one of its aliases is already in use', {
+ method: 'insertOrUpdateEmoji',
+ });
+ }
+
+ if (typeof emojiData.extension === 'undefined') {
+ throw new Meteor.Error('error-the-field-is-required', 'The custom emoji file is required', {
+ method: 'insertOrUpdateEmoji',
+ });
+ }
+
+ if (!emojiData._id) {
+ // insert emoji
+ const createEmoji = {
+ name: emojiData.name,
+ aliases,
+ extension: emojiData.extension,
+ };
+
+ const _id = (await EmojiCustom.create(createEmoji)).insertedId;
+
+ void api.broadcast('emoji.updateCustom', createEmoji);
+
+ return { ...emojiData, ...createEmoji, _id };
+ }
+
+ // update emoji
+ if (emojiData.newFile) {
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`));
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.extension}`));
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`));
+
+ await EmojiCustom.setExtension(emojiData._id, emojiData.extension);
+ } else if (emojiData.name !== emojiData.previousName) {
+ const rs = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(
+ encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`),
+ );
+ if (rs !== null) {
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
+ const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
+ encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`),
+ rs.contentType,
+ );
+ ws.on('end', () =>
+ RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)),
+ );
+ rs.readStream.pipe(ws);
+ }
+ }
+
+ if (emojiData.name !== emojiData.previousName) {
+ await EmojiCustom.setName(emojiData._id, emojiData.name);
+ }
+
+ if (emojiData.aliases) {
+ await EmojiCustom.setAliases(emojiData._id, aliases);
+ } else {
+ await EmojiCustom.setAliases(emojiData._id, []);
+ }
+
+ void api.broadcast('emoji.updateCustom', { ...emojiData, aliases });
+
+ return { ...emojiData, aliases } as EmojiDataWithParsedAliases;
+}
diff --git a/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts
new file mode 100644
index 000000000000..07633eaa1a7d
--- /dev/null
+++ b/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts
@@ -0,0 +1,77 @@
+import { api, Media } from '@rocket.chat/core-services';
+import limax from 'limax';
+import { Meteor } from 'meteor/meteor';
+import sharp from 'sharp';
+
+import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
+import { RocketChatFile } from '../../../file/server';
+import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
+
+const getFile = async (file: Buffer, extension: string) => {
+ if (extension !== 'svg+xml') {
+ return file;
+ }
+
+ return sharp(file).png().toBuffer();
+};
+
+type EmojiData = {
+ _id?: string;
+ name: string;
+ aliases?: string | string[];
+ extension: string;
+ previousName?: string;
+ previousExtension?: string;
+ newFile?: boolean;
+};
+
+export async function uploadEmojiCustom(userId: string | null, binaryContent: string, contentType: string, emojiData: EmojiData) {
+ return uploadEmojiCustomWithBuffer(userId, Buffer.from(binaryContent, 'binary'), contentType, emojiData);
+}
+
+export async function uploadEmojiCustomWithBuffer(
+ userId: string | null,
+ buffer: Buffer,
+ contentType: string,
+ emojiData: EmojiData,
+): Promise {
+ // technically, since this method doesnt have any datatype validations, users can
+ // upload videos as emojis. The FE won't play them, but they will waste space for sure.
+ if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ emojiData.name = limax(emojiData.name, { replacement: '_' });
+ // delete aliases for notification purposes. here, it is a string rather than an array
+ delete emojiData.aliases;
+
+ const file = await getFile(buffer, emojiData.extension);
+ emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
+
+ let fileBuffer;
+ // sharp doesn't support these formats without imagemagick or libvips installed
+ // so they will be stored as they are :(
+ if (['gif', 'x-icon', 'bmp', 'webm'].includes(emojiData.extension)) {
+ fileBuffer = file;
+ } else {
+ // This is to support the idea of having "sticker-like" emojis
+ const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 512, 512, true, false, false, 'inside');
+ fileBuffer = resizedEmojiBuffer;
+ }
+
+ const rs = RocketChatFile.bufferToStream(fileBuffer);
+ await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
+
+ return new Promise((resolve) => {
+ const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
+ encodeURIComponent(`${emojiData.name}.${emojiData.extension}`),
+ contentType,
+ );
+ ws.on('end', () => {
+ setTimeout(() => api.broadcast('emoji.updateCustom', emojiData), 500);
+ resolve();
+ });
+
+ rs.pipe(ws);
+ });
+}
diff --git a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts
index 5d4a6742314b..1891d1b3ed95 100644
--- a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts
+++ b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts
@@ -1,13 +1,7 @@
-import { api } from '@rocket.chat/core-services';
import type { ServerMethods } from '@rocket.chat/ddp-client';
-import { EmojiCustom } from '@rocket.chat/models';
-import limax from 'limax';
import { Meteor } from 'meteor/meteor';
-import _ from 'underscore';
-import { trim } from '../../../../lib/utils/stringUtils';
-import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
-import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
+import { insertOrUpdateEmoji } from '../lib/insertOrUpdateEmoji';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -26,130 +20,12 @@ declare module '@rocket.chat/ddp-client' {
Meteor.methods({
async insertOrUpdateEmoji(emojiData) {
- if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-emoji'))) {
- throw new Meteor.Error('not_authorized');
- }
-
- if (!trim(emojiData.name)) {
- throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', {
- method: 'insertOrUpdateEmoji',
- field: 'Name',
- });
- }
-
- emojiData.name = limax(emojiData.name, { replacement: '_' });
- emojiData.aliases = limax(emojiData.aliases, { replacement: '_' });
-
- // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, )
- // more practical than allowing specific sets of characters; also allows foreign languages
- const nameValidation = /[\s,:><&"'\/\\\(\)]/;
- const aliasValidation = /[:><&\|"'\/\\\(\)]/;
-
- // silently strip colon; this allows for uploading :emojiname: as emojiname
- emojiData.name = emojiData.name.replace(/:/g, '');
- emojiData.aliases = emojiData.aliases.replace(/:/g, '');
-
- if (nameValidation.test(emojiData.name)) {
- throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.name} is not a valid name`, {
- method: 'insertOrUpdateEmoji',
- input: emojiData.name,
- field: 'Name',
- });
- }
-
- let aliases: string[] = [];
- if (emojiData.aliases) {
- if (aliasValidation.test(emojiData.aliases)) {
- throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.aliases} is not a valid alias set`, {
- method: 'insertOrUpdateEmoji',
- input: emojiData.aliases,
- field: 'Alias_Set',
- });
- }
- aliases = _.without(emojiData.aliases.split(/[\s,]/).filter(Boolean), emojiData.name);
- }
-
- emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
-
- let matchingResults = [];
-
- if (emojiData._id) {
- matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray();
- for await (const alias of aliases) {
- matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray());
- }
- } else {
- matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray();
- for await (const alias of aliases) {
- matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray());
- }
- }
-
- if (matchingResults.length > 0) {
- throw new Meteor.Error(
- 'Custom_Emoji_Error_Name_Or_Alias_Already_In_Use',
- 'The custom emoji or one of its aliases is already in use',
- { method: 'insertOrUpdateEmoji' },
- );
- }
-
- if (typeof emojiData.extension === 'undefined') {
- throw new Meteor.Error('error-the-field-is-required', 'The custom emoji file is required', {
- method: 'insertOrUpdateEmoji',
- });
- }
+ const emoji = await insertOrUpdateEmoji(this.userId, emojiData);
if (!emojiData._id) {
- // insert emoji
- const createEmoji = {
- name: emojiData.name,
- aliases,
- extension: emojiData.extension,
- };
-
- const _id = (await EmojiCustom.create(createEmoji)).insertedId;
-
- void api.broadcast('emoji.updateCustom', createEmoji);
-
- return _id;
+ return emoji._id;
}
- // update emoji
- if (emojiData.newFile) {
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`));
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.extension}`));
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`));
-
- await EmojiCustom.setExtension(emojiData._id, emojiData.extension);
- } else if (emojiData.name !== emojiData.previousName) {
- const rs = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(
- encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`),
- );
- if (rs !== null) {
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
- const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
- encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`),
- rs.contentType,
- );
- ws.on('end', () =>
- RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)),
- );
- rs.readStream.pipe(ws);
- }
- }
-
- if (emojiData.name !== emojiData.previousName) {
- await EmojiCustom.setName(emojiData._id, emojiData.name);
- }
-
- if (emojiData.aliases) {
- await EmojiCustom.setAliases(emojiData._id, aliases);
- } else {
- await EmojiCustom.setAliases(emojiData._id, []);
- }
-
- void api.broadcast('emoji.updateCustom', emojiData);
- return true;
+ return !!emoji;
},
});
diff --git a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts
index a46f457cd70f..e387888b1311 100644
--- a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts
+++ b/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts
@@ -1,20 +1,7 @@
-import { api, Media } from '@rocket.chat/core-services';
import type { ServerMethods } from '@rocket.chat/ddp-client';
-import limax from 'limax';
import { Meteor } from 'meteor/meteor';
-import sharp from 'sharp';
-import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
-import { RocketChatFile } from '../../../file/server';
-import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
-
-const getFile = async (file: Buffer, extension: string) => {
- if (extension !== 'svg+xml') {
- return file;
- }
-
- return sharp(file).png().toBuffer();
-};
+import { uploadEmojiCustom } from '../lib/uploadEmojiCustom';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -33,44 +20,6 @@ declare module '@rocket.chat/ddp-client' {
Meteor.methods({
async uploadEmojiCustom(binaryContent, contentType, emojiData) {
- // technically, since this method doesnt have any datatype validations, users can
- // upload videos as emojis. The FE won't play them, but they will waste space for sure.
- if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-emoji'))) {
- throw new Meteor.Error('not_authorized');
- }
-
- emojiData.name = limax(emojiData.name, { replacement: '_' });
- // delete aliases for notification purposes. here, it is a string rather than an array
- delete emojiData.aliases;
-
- const file = await getFile(Buffer.from(binaryContent, 'binary'), emojiData.extension);
- emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
-
- let fileBuffer;
- // sharp doesn't support these formats without imagemagick or libvips installed
- // so they will be stored as they are :(
- if (['gif', 'x-icon', 'bmp', 'webm'].includes(emojiData.extension)) {
- fileBuffer = file;
- } else {
- // This is to support the idea of having "sticker-like" emojis
- const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 512, 512, true, false, false, 'inside');
- fileBuffer = resizedEmojiBuffer;
- }
-
- const rs = RocketChatFile.bufferToStream(fileBuffer);
- await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
-
- return new Promise((resolve) => {
- const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
- encodeURIComponent(`${emojiData.name}.${emojiData.extension}`),
- contentType,
- );
- ws.on('end', () => {
- setTimeout(() => api.broadcast('emoji.updateCustom', emojiData), 500);
- resolve();
- });
-
- rs.pipe(ws);
- });
+ await uploadEmojiCustom(this.userId, binaryContent, contentType, emojiData);
},
});
diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js
index 6c7fa5cad0e8..4f2a197b25ee 100644
--- a/apps/meteor/app/federation/server/endpoints/dispatch.js
+++ b/apps/meteor/app/federation/server/endpoints/dispatch.js
@@ -6,7 +6,13 @@ import EJSON from 'ejson';
import { API } from '../../../api/server';
import { FileUpload } from '../../../file-upload/server';
import { deleteRoom } from '../../../lib/server/functions/deleteRoom';
-import { notifyOnMessageChange, notifyOnRoomChanged, notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener';
+import {
+ notifyOnMessageChange,
+ notifyOnRoomChanged,
+ notifyOnRoomChangedById,
+ notifyOnSubscriptionChanged,
+ notifyOnSubscriptionChangedById,
+} 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';
@@ -141,7 +147,10 @@ const eventHandlers = {
const denormalizedSubscription = normalizers.denormalizeSubscription(subscription);
// Create the subscription
- await Subscriptions.insertOne(denormalizedSubscription);
+ const { insertedId } = await Subscriptions.insertOne(denormalizedSubscription);
+ if (insertedId) {
+ void notifyOnSubscriptionChangedById(insertedId);
+ }
federationAltered = true;
}
} catch (ex) {
@@ -176,7 +185,10 @@ const eventHandlers = {
} = event;
// Remove the user's subscription
- await Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
+ const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
+ if (deletedSubscription) {
+ void notifyOnSubscriptionChanged(deletedSubscription, 'removed');
+ }
// Refresh the servers list
await FederationServers.refreshServers();
@@ -204,7 +216,10 @@ const eventHandlers = {
} = event;
// Remove the user's subscription
- await Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
+ const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
+ if (deletedSubscription) {
+ void notifyOnSubscriptionChanged(deletedSubscription, 'removed');
+ }
// Refresh the servers list
await FederationServers.refreshServers();
@@ -293,8 +308,12 @@ const eventHandlers = {
await processThreads(denormalizedMessage, room);
- // Notify users
- await notifyUsersOnMessage(denormalizedMessage, room);
+ const roomUpdater = Rooms.getUpdater();
+ await notifyUsersOnMessage(denormalizedMessage, room, roomUpdater);
+ if (roomUpdater.hasChanges()) {
+ await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater);
+ }
+
sendAllNotifications(denormalizedMessage, room);
messageForNotification = denormalizedMessage;
} catch (err) {
diff --git a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js b/apps/meteor/app/federation/server/hooks/afterSaveMessage.js
index 7f67f4770686..20c64f87dda8 100644
--- a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js
+++ b/apps/meteor/app/federation/server/hooks/afterSaveMessage.js
@@ -6,7 +6,7 @@ import { getFederationDomain } from '../lib/getFederationDomain';
import { clientLogger } from '../lib/logger';
import { normalizers } from '../normalizers';
-async function afterSaveMessage(message, room) {
+async function afterSaveMessage(message, { room }) {
// If there are not federated users on this room, ignore it
if (!hasExternalDomain(room)) {
return message;
diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts
index 08e2ccb0a52b..8714c71f20d6 100644
--- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts
+++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts
@@ -10,7 +10,7 @@ import URL from 'url';
import { hashLoginToken } from '@rocket.chat/account-utils';
import { Apps, AppEvents } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
-import type { IUpload } from '@rocket.chat/core-typings';
+import { isE2EEUpload, type IUpload } from '@rocket.chat/core-typings';
import { Users, Avatars, UserDataFiles, Uploads, Settings, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
import type { NextFunction } from 'connect';
import filesize from 'filesize';
@@ -170,7 +170,13 @@ export const FileUpload = {
throw new Meteor.Error('error-file-too-large', reason);
}
- if (!fileUploadIsValidContentType(file?.type)) {
+ if (!settings.get('E2E_Enable_Encrypt_Files') && isE2EEUpload(file)) {
+ const reason = i18n.t('Encrypted_file_not_allowed', { lng: language });
+ throw new Meteor.Error('error-invalid-file-type', reason);
+ }
+
+ // E2EE files are of type - application/octet-stream, application/octet-stream is whitelisted for E2EE files.
+ if (!fileUploadIsValidContentType(file?.type, isE2EEUpload(file) ? 'application/octet-stream' : undefined)) {
const reason = i18n.t('File_type_is_not_accepted', { lng: language });
throw new Meteor.Error('error-invalid-file-type', reason);
}
diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
index 7b1e71eaa0f0..6de47e33b2b6 100644
--- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
+++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
@@ -28,7 +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 { notifyOnSubscriptionChangedByRoomId, 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';
@@ -1161,8 +1161,11 @@ export class ImportDataConverter {
}
async archiveRoomById(rid: string) {
- await Rooms.archiveById(rid);
- await Subscriptions.archiveByRoomId(rid);
+ const responses = await Promise.all([Rooms.archiveById(rid), Subscriptions.archiveByRoomId(rid)]);
+
+ if (responses[1]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
}
async convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise {
diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
index b6d977dc36e2..3fb9c419aa5f 100644
--- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
+++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
@@ -5,6 +5,7 @@ import { Subscriptions } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig';
import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref';
+import { notifyOnSubscriptionChangedById } from '../lib/notifyListener';
import { getDefaultChannels } from './getDefaultChannels';
export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise {
@@ -14,8 +15,9 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?:
for await (const room of defaultRooms) {
if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) {
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user);
+
// Add a subscription to this user
- await Subscriptions.createWithRoomAndUser(room, user, {
+ const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, {
ts: new Date(),
open: true,
alert: true,
@@ -27,6 +29,10 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?:
...getDefaultSubscriptionPref(user),
});
+ if (insertedId) {
+ void notifyOnSubscriptionChangedById(insertedId, 'inserted');
+ }
+
// Insert user joined message
if (!silenced) {
await Message.saveSystemMessage('uj', room._id, user.username || '', user);
diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
index b6ffc0ca4629..e6ca7b2a8b4d 100644
--- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts
+++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
@@ -11,7 +11,7 @@ import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/li
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
import { settings } from '../../../settings/server';
import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref';
-import { notifyOnRoomChangedById } from '../lib/notifyListener';
+import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener';
export const addUserToRoom = async function (
rid: string,
@@ -82,7 +82,7 @@ export const addUserToRoom = async function (
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded);
- await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, {
+ const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, {
ts: now,
open: true,
alert: !skipAlertSound,
@@ -93,6 +93,10 @@ export const addUserToRoom = async function (
...getDefaultSubscriptionPref(userToBeAdded as IUser),
});
+ if (insertedId) {
+ void notifyOnSubscriptionChangedById(insertedId, 'inserted');
+ }
+
void notifyOnRoomChangedById(rid);
if (!userToBeAdded.username) {
diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts
index 3378d69f99ff..46fd7a1ac35b 100644
--- a/apps/meteor/app/lib/server/functions/archiveRoom.ts
+++ b/apps/meteor/app/lib/server/functions/archiveRoom.ts
@@ -3,11 +3,16 @@ 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';
+import { notifyOnRoomChanged, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener';
export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise {
await Rooms.archiveById(rid);
- await Subscriptions.archiveByRoomId(rid);
+
+ const archiveResponse = await Subscriptions.archiveByRoomId(rid);
+ if (archiveResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
await Message.saveSystemMessage('room-archived', rid, '', user);
const room = await Rooms.findOneById(rid);
diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts
index 2bfb1086c635..765a03cad87b 100644
--- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts
+++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts
@@ -4,7 +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 { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener';
import { deleteRoom } from './deleteRoom';
export async function cleanRoomHistory({
@@ -75,6 +75,7 @@ export async function cleanRoomHistory({
if (!ignoreThreads) {
const threads = new Set();
+
await Messages.findThreadsByRoomIdPinnedTimestampAndUsers(
{ rid, pinned: excludePinned, ignoreDiscussion, ts, users: fromUsers },
{ projection: { _id: 1 } },
@@ -83,7 +84,14 @@ export async function cleanRoomHistory({
});
if (threads.size > 0) {
- await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]);
+ const subscriptionIds: string[] = (
+ await Subscriptions.findUnreadThreadsByRoomId(rid, [...threads], { projection: { _id: 1 } }).toArray()
+ ).map(({ _id }) => _id);
+
+ const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]);
+ if (modifiedCount) {
+ subscriptionIds.forEach((id) => notifyOnSubscriptionChangedById(id));
+ }
}
}
diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
index b716be044d57..263b137ae00c 100644
--- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
+++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
@@ -5,6 +5,7 @@ import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';
+import { notifyOnSubscriptionChanged } from '../lib/notifyListener';
export const closeLivechatRoom = async (
user: IUser,
@@ -34,9 +35,12 @@ export const closeLivechatRoom = async (
}
if (!room.open) {
- const subscriptionsLeft = await Subscriptions.countByRoomId(roomId);
- if (subscriptionsLeft) {
- await Subscriptions.removeByRoomId(roomId);
+ const { deletedCount } = await Subscriptions.removeByRoomId(roomId, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ });
+ if (deletedCount) {
return;
}
throw new Error('error-room-already-closed');
diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts
index 67c6328f38f4..f77ee1f55901 100644
--- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts
+++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts
@@ -11,7 +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';
+import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../lib/notifyListener';
const generateSubscription = (
fname: string,
@@ -135,7 +135,7 @@ export async function createDirectRoom(
if (roomMembers.length === 1) {
// dm to yourself
- await Subscriptions.updateOne(
+ const { modifiedCount, upsertedCount } = await Subscriptions.updateOne(
{ rid, 'u._id': roomMembers[0]._id },
{
$set: { open: true },
@@ -146,6 +146,9 @@ export async function createDirectRoom(
},
{ upsert: true },
);
+ if (modifiedCount || upsertedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, roomMembers[0]._id, modifiedCount ? 'updated' : 'inserted');
+ }
} else {
const memberIds = roomMembers.map((member) => member._id);
const membersWithPreferences: IUser[] = await Users.find(
@@ -155,7 +158,7 @@ export async function createDirectRoom(
for await (const member of membersWithPreferences) {
const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id);
- await Subscriptions.updateOne(
+ const { modifiedCount, upsertedCount } = await Subscriptions.updateOne(
{ rid, 'u._id': member._id },
{
...(options?.creator === member._id && { $set: { open: true } }),
@@ -166,6 +169,9 @@ export async function createDirectRoom(
},
{ upsert: true },
);
+ if (modifiedCount || upsertedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, member._id, modifiedCount ? 'updated' : 'inserted');
+ }
}
}
diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts
index 183cb789051f..769155b66b60 100644
--- a/apps/meteor/app/lib/server/functions/createRoom.ts
+++ b/apps/meteor/app/lib/server/functions/createRoom.ts
@@ -1,7 +1,7 @@
/* eslint-disable complexity */
import { AppEvents, Apps } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
-import { Message, Team } from '@rocket.chat/core-services';
+import { Federation, FederationEE, License, Message, Team } from '@rocket.chat/core-services';
import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services';
import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings';
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
@@ -12,7 +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 { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener';
import { createDirectRoom } from './createDirectRoom';
const isValidName = (name: unknown): name is string => {
@@ -47,7 +47,11 @@ async function createUsersSubscriptions({
...getDefaultSubscriptionPref(owner),
};
- await Subscriptions.createWithRoomAndUser(room, owner, extra);
+ const { insertedId } = await Subscriptions.createWithRoomAndUser(room, owner, extra);
+
+ if (insertedId) {
+ await notifyOnRoomChanged(room, 'inserted');
+ }
return;
}
@@ -98,7 +102,9 @@ async function createUsersSubscriptions({
await Users.addRoomByUserIds(memberIds, room._id);
}
- await Subscriptions.createWithRoomAndManyUsers(room, subs);
+ const { insertedIds } = await Subscriptions.createWithRoomAndManyUsers(room, subs);
+
+ Object.values(insertedIds).forEach((subId) => notifyOnSubscriptionChangedById(subId, 'inserted'));
await Rooms.incUsersCountById(room._id, subs.length);
}
@@ -224,6 +230,13 @@ export const createRoom = async (
Object.assign(roomProps, eventResult);
}
+ const shouldBeHandledByFederation = roomProps.federated === true || owner.username.includes(':');
+
+ if (shouldBeHandledByFederation) {
+ const federation = (await License.hasValidLicense()) ? FederationEE : Federation;
+ await federation.beforeCreateRoom(roomProps);
+ }
+
if (type === 'c') {
await callbacks.run('beforeCreateChannel', owner, roomProps);
}
@@ -232,8 +245,6 @@ export const createRoom = async (
void notifyOnRoomChanged(room, 'inserted');
- const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':');
-
await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation });
if (type === 'c') {
diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts
index 04542d5f1d27..a91e77858043 100644
--- a/apps/meteor/app/lib/server/functions/deleteMessage.ts
+++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts
@@ -1,5 +1,5 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
-import { api } from '@rocket.chat/core-services';
+import { api, Message } from '@rocket.chat/core-services';
import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings';
import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
@@ -35,10 +35,18 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise {
await FileUpload.removeFilesByRoomId(rid);
+
await Messages.removeByRoomId(rid);
+
await callbacks.run('beforeDeleteRoom', rid);
- await Subscriptions.removeByRoomId(rid);
+
+ await Subscriptions.removeByRoomId(rid, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ });
+
await FileUpload.getStore('Avatars').deleteByRoomId(rid);
+
await callbacks.run('afterDeleteRoom', rid);
- await Rooms.removeById(rid);
- void notifyOnRoomChangedById(rid, 'removed');
+ const { deletedCount } = await Rooms.removeById(rid);
+ if (deletedCount) {
+ 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 d6457664671a..483085d40811 100644
--- a/apps/meteor/app/lib/server/functions/deleteUser.ts
+++ b/apps/meteor/app/lib/server/functions/deleteUser.ts
@@ -1,5 +1,5 @@
import { api } from '@rocket.chat/core-services';
-import type { IUser } from '@rocket.chat/core-typings';
+import { isUserFederated, type IUser } from '@rocket.chat/core-typings';
import {
Integrations,
FederationServers,
@@ -12,6 +12,7 @@ import {
ReadReceipts,
LivechatUnitMonitors,
ModerationReports,
+ MatrixBridgedUser,
} from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
@@ -46,6 +47,19 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele
return;
}
+ if (isUserFederated(user)) {
+ throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', {
+ method: 'deleteUser',
+ });
+ }
+
+ const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId);
+ if (remoteUser) {
+ throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', {
+ method: 'deleteUser',
+ });
+ }
+
const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId);
if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) {
@@ -98,7 +112,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele
const rids = subscribedRooms.map((room) => room.rid);
void notifyOnRoomChangedById(rids);
- await Subscriptions.removeByUserId(userId); // Remove user subscriptions
+ await Subscriptions.removeByUserId(userId);
// Remove user as livechat agent
if (user.roles.includes('livechat-agent')) {
diff --git a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
index 75b232462077..8f1981ca386d 100644
--- a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
+++ b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
@@ -1,6 +1,7 @@
import { Messages, Roles, Rooms, Subscriptions, ReadReceipts } from '@rocket.chat/models';
import { FileUpload } from '../../../file-upload/server';
+import { notifyOnSubscriptionChanged } from '../lib/notifyListener';
import type { SubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner';
const bulkRoomCleanUp = async (rids: string[]): Promise => {
@@ -8,7 +9,11 @@ const bulkRoomCleanUp = async (rids: string[]): Promise => {
await Promise.all(rids.map((rid) => FileUpload.removeFilesByRoomId(rid)));
return Promise.all([
- Subscriptions.removeByRoomIds(rids),
+ Subscriptions.removeByRoomIds(rids, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ }),
Messages.removeByRoomIds(rids),
ReadReceipts.removeByRoomIds(rids),
Rooms.removeByIds(rids),
diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
index c55ee382f10c..5800cb68af81 100644
--- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
+++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
@@ -1,6 +1,6 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
-import { Message, Team } from '@rocket.chat/core-services';
+import { Message, Team, Room } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
@@ -8,7 +8,7 @@ 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';
+import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener';
export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise {
const room = await Rooms.findOneById(rid);
@@ -27,6 +27,9 @@ export const removeUserFromRoom = async function (rid: string, user: IUser, opti
throw error;
}
+ await Room.beforeLeave(room);
+
+ // TODO: move before callbacks to service
await beforeLeaveRoomCallback.run(user, room);
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id, {
@@ -56,7 +59,10 @@ export const removeUserFromRoom = async function (rid: string, user: IUser, opti
await Message.saveSystemMessage('command', rid, 'survey', user);
}
- await Subscriptions.removeByRoomIdAndUserId(rid, user._id);
+ const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, user._id);
+ if (deletedSubscription) {
+ void notifyOnSubscriptionChanged(deletedSubscription, 'removed');
+ }
if (room.teamId && room.teamMain) {
await Team.removeMember(room.teamId, user._id);
diff --git a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts
index 4a0ac005e55c..5383048f13bd 100644
--- a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts
+++ b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts
@@ -5,6 +5,7 @@ import type { UpdateFilter } from 'mongodb';
import { trim } from '../../../../lib/utils/stringUtils';
import { settings } from '../../../settings/server';
+import { notifyOnSubscriptionChangedByUserIdAndRoomType } from '../lib/notifyListener';
export const saveCustomFieldsWithoutValidation = async function (userId: string, formData: Record): Promise {
if (trim(settings.get('Accounts_CustomFields')) !== '') {
@@ -22,7 +23,10 @@ export const saveCustomFieldsWithoutValidation = async function (userId: string,
await Users.setCustomFields(userId, customFields);
// Update customFields of all Direct Messages' Rooms for userId
- await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields);
+ const setCustomFieldsResponse = await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields);
+ if (setCustomFieldsResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByUserIdAndRoomType(userId, 'd');
+ }
for await (const fieldName of Object.keys(customFields)) {
if (!customFieldsMeta[fieldName].modifyRecordField) {
diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
index 0b9ff21e53e3..1729a1ba8abd 100644
--- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
+++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
@@ -3,7 +3,11 @@ 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 {
+ notifyOnRoomChangedByUsernamesOrUids,
+ notifyOnSubscriptionChangedByUserId,
+ notifyOnSubscriptionChangedByNameAndRoomType,
+} from '../lib/notifyListener';
import { _setRealName } from './setRealName';
import { _setUsername } from './setUsername';
import { updateGroupDMsName } from './updateGroupDMsName';
@@ -129,20 +133,38 @@ async function updateUsernameReferences({
await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg);
}
- await Rooms.replaceUsername(previousUsername, username);
- await Rooms.replaceMutedUsername(previousUsername, username);
- await Rooms.replaceUsernameOfUserByUserId(user._id, username);
- await Subscriptions.setUserUsernameByUserId(user._id, username);
+ const responses = await Promise.all([
+ Rooms.replaceUsername(previousUsername, username),
+ Rooms.replaceMutedUsername(previousUsername, username),
+ Rooms.replaceUsernameOfUserByUserId(user._id, username),
+ Subscriptions.setUserUsernameByUserId(user._id, username),
+ LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username),
+ ]);
- await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username);
+ if (responses[3]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByUserId(user._id);
+ }
- void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]);
+ if (responses[0]?.modifiedCount || responses[1]?.modifiedCount || responses[2]?.modifiedCount) {
+ void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]);
+ }
}
// update other references if either the name or username has changed
if (usernameChanged || nameChanged) {
// update name and fname of 1-on-1 direct messages
- await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name);
+ const updateDirectNameResponse = await Subscriptions.updateDirectNameAndFnameByName(
+ previousUsername,
+ rawUsername && username,
+ rawName && name,
+ );
+
+ if (updateDirectNameResponse?.modifiedCount) {
+ void notifyOnSubscriptionChangedByNameAndRoomType({
+ t: 'd',
+ name: username,
+ });
+ }
// update name and fname of group direct messages
await updateGroupDMsName(user);
diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts
index 4a5b8313ebcd..aba5ddb7264c 100644
--- a/apps/meteor/app/lib/server/functions/sendMessage.ts
+++ b/apps/meteor/app/lib/server/functions/sendMessage.ts
@@ -4,12 +4,12 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { Messages } from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
-import { callbacks } from '../../../../lib/callbacks';
import { isRelativeURL } from '../../../../lib/utils/isRelativeURL';
import { isURL } from '../../../../lib/utils/isURL';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { FileUpload } from '../../../file-upload/server';
import { settings } from '../../../settings/server';
+import { afterSaveMessage } from '../lib/afterSaveMessage';
import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener';
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';
import { parseUrlsInMessage } from './parseUrlsInMessage';
@@ -289,11 +289,10 @@ export const sendMessage = async function (user: any, message: any, room: any, u
void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageSent', message);
}
- await callbacks.run('afterSaveMessage', message, room);
+ // TODO: is there an opportunity to send returned data to notifyOnMessageChange?
+ await afterSaveMessage(message, room);
- void notifyOnMessageChange({
- id: message._id,
- });
+ void notifyOnMessageChange({ id: message._id });
void notifyOnRoomChangedById(message.rid);
diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts
index e3104db280dd..929c24210d2d 100644
--- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts
+++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts
@@ -1,6 +1,7 @@
+import { Federation, FederationEE, License } from '@rocket.chat/core-services';
import type { IUser, IUserEmail } from '@rocket.chat/core-typings';
import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings';
-import { Rooms, Users, Subscriptions } from '@rocket.chat/models';
+import { Rooms, Users, Subscriptions, MatrixBridgedUser } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -8,7 +9,12 @@ 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 {
+ notifyOnRoomChangedById,
+ notifyOnRoomChangedByUserDM,
+ notifyOnSubscriptionChangedByNameAndRoomType,
+ notifyOnUserChange,
+} from '../lib/notifyListener';
import { closeOmnichannelConversations } from './closeOmnichannelConversations';
import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner';
import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms';
@@ -38,8 +44,10 @@ async function reactivateDirectConversations(userId: string) {
return acc;
}, []);
- await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false);
- void notifyOnRoomChangedById(roomsToReactivate);
+ const setDmReadOnlyResponse = await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false);
+ if (setDmReadOnlyResponse.modifiedCount) {
+ void notifyOnRoomChangedById(roomsToReactivate);
+ }
}
export async function setUserActiveStatus(userId: string, active: boolean, confirmRelinquish = false): Promise {
@@ -58,6 +66,22 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi
});
}
+ if (user.active !== active) {
+ const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId);
+
+ if (remoteUser) {
+ if (active) {
+ throw new Meteor.Error('error-not-allowed', 'Deactivated federated users can not be re-activated', {
+ method: 'setUserActiveStatus',
+ });
+ }
+
+ const federation = (await License.hasValidLicense()) ? FederationEE : Federation;
+
+ await federation.deactivateRemoteUser(remoteUser);
+ }
+ }
+
// Users without username can't do anything, so there is no need to check for owned rooms
if (user.username != null && !active) {
const userAdmin = await Users.findOneAdmin(userId || '');
@@ -101,7 +125,10 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi
}
if (user.username) {
- await Subscriptions.setArchivedByUsername(user.username, !active);
+ const { modifiedCount } = await Subscriptions.setArchivedByUsername(user.username, !active);
+ if (modifiedCount) {
+ void notifyOnSubscriptionChangedByNameAndRoomType({ t: 'd', name: user.username });
+ }
}
if (active === false) {
diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts
index e19ef874db0f..5b2b1923da75 100644
--- a/apps/meteor/app/lib/server/functions/setUsername.ts
+++ b/apps/meteor/app/lib/server/functions/setUsername.ts
@@ -17,6 +17,7 @@ import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser';
import { joinDefaultChannels } from './joinDefaultChannels';
import { saveUserIdentity } from './saveUserIdentity';
import { setUserAvatar } from './setUserAvatar';
+import { validateUsername } from './validateUsername';
export const setUsernameWithValidation = async (userId: string, username: string, joinDefaultChannelsSilenced?: boolean): Promise => {
if (!username) {
@@ -37,14 +38,7 @@ export const setUsernameWithValidation = async (userId: string, username: string
return;
}
- let nameValidation;
- try {
- nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`);
- } catch (error) {
- nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
- }
-
- if (!nameValidation.test(username)) {
+ if (!validateUsername(username)) {
throw new Meteor.Error(
'username-invalid',
`${_.escape(username)} is not a valid username, use only letters, numbers, dots, hyphens and underscores`,
@@ -74,18 +68,15 @@ export const setUsernameWithValidation = async (userId: string, username: string
export const _setUsername = async function (userId: string, u: string, fullUser: IUser): Promise {
const username = u.trim();
+
if (!userId || !username) {
return false;
}
- let nameValidation;
- try {
- nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`);
- } catch (error) {
- nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
- }
- if (!nameValidation.test(username)) {
+
+ if (!validateUsername(username)) {
return false;
}
+
const user = fullUser || (await Users.findOneById(userId));
// User already has desired username, return
if (user.username === username) {
diff --git a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts
index 7db86ed933a3..699f9c3701b1 100644
--- a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts
+++ b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts
@@ -2,11 +2,16 @@ 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';
+import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener';
export const unarchiveRoom = async function (rid: string, user: IMessage['u']): Promise {
await Rooms.unarchiveById(rid);
- await Subscriptions.unarchiveByRoomId(rid);
+
+ const unarchiveResponse = await Subscriptions.unarchiveByRoomId(rid);
+ if (unarchiveResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
await Message.saveSystemMessage('room-unarchived', rid, '', user);
void notifyOnRoomChangedById(rid);
diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts
index a0ad2eedcf55..feb26ce6a1b0 100644
--- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts
+++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts
@@ -1,6 +1,8 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
+import { notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener';
+
const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', ');
const getName = (members: IUser[]): string => members.map(({ username }) => username).join(',');
@@ -63,7 +65,10 @@ export const updateGroupDMsName = async (userThatChangedName: IUser): Promise _id !== sub.u._id);
- await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers));
+ const updateNameRespose = await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers));
+ if (updateNameRespose.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(room._id);
+ }
}
}
};
diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts
index f51fc83c11d7..96683d40348f 100644
--- a/apps/meteor/app/lib/server/functions/updateMessage.ts
+++ b/apps/meteor/app/lib/server/functions/updateMessage.ts
@@ -4,14 +4,14 @@ import type { IMessage, IUser, AtLeast } from '@rocket.chat/core-typings';
import { Messages, Rooms } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
-import { callbacks } from '../../../../lib/callbacks';
import { settings } from '../../../settings/server';
+import { afterSaveMessage } from '../lib/afterSaveMessage';
import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener';
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';
import { parseUrlsInMessage } from './parseUrlsInMessage';
export const updateMessage = async function (
- message: AtLeast,
+ message: AtLeast,
user: IUser,
originalMsg?: IMessage,
previewUrls?: string[],
@@ -99,11 +99,11 @@ export const updateMessage = async function (
// 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);
+ const data = await afterSaveMessage(msg, room, user._id);
void notifyOnMessageChange({
id: msg._id,
- data: data as any, // TODO move "afterSaveMessage" type definition to specify a return value
+ data,
});
if (room?.lastMessage?._id === msg._id) {
diff --git a/apps/meteor/app/lib/server/functions/validateUsername.ts b/apps/meteor/app/lib/server/functions/validateUsername.ts
new file mode 100644
index 000000000000..523667282d22
--- /dev/null
+++ b/apps/meteor/app/lib/server/functions/validateUsername.ts
@@ -0,0 +1,15 @@
+import { settings } from '../../../settings/server';
+
+export const validateUsername = (username: string): boolean => {
+ const settingsRegExp = settings.get('UTF8_User_Names_Validation');
+ const defaultPattern = /^[0-9a-zA-Z-_.]+$/;
+
+ let usernameRegExp: RegExp;
+ try {
+ usernameRegExp = settingsRegExp ? new RegExp(`^${settingsRegExp}$`) : defaultPattern;
+ } catch (e) {
+ usernameRegExp = defaultPattern;
+ }
+
+ return usernameRegExp.test(username);
+};
diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts
index 80aaa2a64a9e..49fad2002c75 100644
--- a/apps/meteor/app/lib/server/index.ts
+++ b/apps/meteor/app/lib/server/index.ts
@@ -49,5 +49,6 @@ import './methods/unarchiveRoom';
import './methods/unblockUser';
import './methods/updateMessage';
import './methods/saveCustomFields';
+import './methods/checkFederationConfiguration';
export * from './lib';
diff --git a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts
new file mode 100644
index 000000000000..5b6e12b1e185
--- /dev/null
+++ b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts
@@ -0,0 +1,35 @@
+import type { IMessage, IUser, IRoom } from '@rocket.chat/core-typings';
+import type { Updater } from '@rocket.chat/models';
+import { Rooms } from '@rocket.chat/models';
+
+import { callbacks } from '../../../../lib/callbacks';
+
+export async function afterSaveMessage(
+ message: IMessage,
+ room: IRoom,
+ uid?: IUser['_id'],
+ roomUpdater?: Updater,
+): Promise {
+ const updater = roomUpdater ?? Rooms.getUpdater();
+ const data = await callbacks.run('afterSaveMessage', message, { room, uid, roomUpdater: updater });
+
+ if (!roomUpdater && updater.hasChanges()) {
+ await Rooms.updateFromUpdater({ _id: room._id }, updater);
+ }
+
+ // TODO: Fix type - callback configuration needs to be updated
+ return data as unknown as IMessage;
+}
+
+export function afterSaveMessageAsync(
+ message: IMessage,
+ room: IRoom,
+ uid?: IUser['_id'],
+ roomUpdater: Updater = Rooms.getUpdater(),
+): void {
+ callbacks.runAsync('afterSaveMessage', message, { room, uid, roomUpdater });
+
+ if (roomUpdater.hasChanges()) {
+ void Rooms.updateFromUpdater({ _id: room._id }, roomUpdater);
+ }
+}
diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts
index 83ab5774374a..778fe89dbbf4 100644
--- a/apps/meteor/app/lib/server/lib/notifyListener.ts
+++ b/apps/meteor/app/lib/server/lib/notifyListener.ts
@@ -15,6 +15,7 @@ import type {
IEmailInbox,
IIntegrationHistory,
AtLeast,
+ ISubscription,
ISettingColor,
IUser,
IMessage,
@@ -30,6 +31,7 @@ import {
Integrations,
LoginServiceConfiguration,
IntegrationHistory,
+ Subscriptions,
LivechatInquiry,
LivechatDepartmentAgents,
Users,
@@ -37,6 +39,7 @@ import {
} from '@rocket.chat/models';
import mem from 'mem';
+import { subscriptionFields } from '../../../../lib/publishFields';
import { shouldHideSystemMessage } from '../../../../server/lib/systemMessage/hideSystemMessage';
type ClientAction = 'inserted' | 'updated' | 'removed';
@@ -461,12 +464,125 @@ export async function getMessageToBroadcast({ id, data }: { id: IMessage['_id'];
}
export const notifyOnMessageChange = withDbWatcherCheck(async ({ id, data }: { id: IMessage['_id']; data?: IMessage }): Promise => {
- if (!dbWatchersDisabled) {
- return;
- }
const message = await getMessageToBroadcast({ id, data });
if (!message) {
return;
}
void api.broadcast('watch.messages', { message });
});
+
+export const notifyOnSubscriptionChanged = withDbWatcherCheck(
+ async (subscription: ISubscription, clientAction: ClientAction = 'updated'): Promise => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ },
+);
+
+export const notifyOnSubscriptionChangedByRoomIdAndUserId = withDbWatcherCheck(
+ async (
+ rid: ISubscription['rid'],
+ uid: ISubscription['u']['_id'],
+ clientAction: Exclude = 'updated',
+ ): Promise => {
+ const cursor = Subscriptions.findByUserIdAndRoomIds(uid, [rid], { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedById = withDbWatcherCheck(
+ async (id: ISubscription['_id'], clientAction: Exclude = 'updated'): Promise => {
+ const subscription = await Subscriptions.findOneById(id);
+ if (!subscription) {
+ return;
+ }
+
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ },
+);
+
+export const notifyOnSubscriptionChangedByUserPreferences = withDbWatcherCheck(
+ async (
+ uid: ISubscription['u']['_id'],
+ notificationOriginField: keyof ISubscription,
+ originFieldNotEqualValue: 'user' | 'subscription',
+ clientAction: Exclude = 'updated',
+ ): Promise => {
+ const cursor = Subscriptions.findByUserPreferences(uid, notificationOriginField, originFieldNotEqualValue, {
+ projection: subscriptionFields,
+ });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByRoomId = withDbWatcherCheck(
+ async (rid: ISubscription['rid'], clientAction: Exclude = 'updated'): Promise => {
+ const cursor = Subscriptions.findByRoomId(rid, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByAutoTranslateAndUserId = withDbWatcherCheck(
+ async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => {
+ const cursor = Subscriptions.findByAutoTranslateAndUserId(uid, true, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck(
+ async (
+ uid: ISubscription['u']['_id'],
+ t: ISubscription['t'],
+ clientAction: Exclude = 'updated',
+ ): Promise => {
+ const cursor = Subscriptions.findByUserIdAndRoomType(uid, t, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck(
+ async (filter: Partial>, clientAction: Exclude = 'updated'): Promise => {
+ const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByUserId = withDbWatcherCheck(
+ async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => {
+ const cursor = Subscriptions.findByUserId(uid, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
+
+export const notifyOnSubscriptionChangedByRoomIdAndUserIds = withDbWatcherCheck(
+ async (
+ rid: ISubscription['rid'],
+ uids: ISubscription['u']['_id'][],
+ clientAction: Exclude = 'updated',
+ ): Promise => {
+ const cursor = Subscriptions.findByRoomIdAndUserIds(rid, uids, { projection: subscriptionFields });
+
+ void cursor.forEach((subscription) => {
+ void api.broadcast('watch.subscriptions', { clientAction, subscription });
+ });
+ },
+);
diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts
index 76a18eba3362..7551cabb6e63 100644
--- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts
+++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts
@@ -1,11 +1,17 @@
import type { IMessage, IRoom, IUser, RoomType } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
+import type { Updater } from '@rocket.chat/models';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import moment from 'moment';
import { callbacks } from '../../../../lib/callbacks';
import { settings } from '../../../settings/server';
+import {
+ notifyOnSubscriptionChanged,
+ notifyOnSubscriptionChangedByRoomIdAndUserId,
+ notifyOnSubscriptionChangedByRoomIdAndUserIds,
+} from './notifyListener';
function messageContainsHighlight(message: IMessage, highlights: string[]): boolean {
if (!highlights || highlights.length === 0) return false;
@@ -50,26 +56,14 @@ export async function getMentions(message: IMessage): Promise<{ toAll: boolean;
type UnreadCountType = 'all_messages' | 'user_mentions_only' | 'group_mentions_only' | 'user_and_group_mentions_only';
-const incGroupMentions = async (
- rid: IRoom['_id'],
- roomType: RoomType,
- excludeUserId: IUser['_id'],
- unreadCount: Exclude,
-): Promise => {
+const getGroupMentions = (roomType: RoomType, unreadCount: Exclude): number => {
const incUnreadByGroup = ['all_messages', 'group_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount);
- const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0;
- await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(rid, excludeUserId, 1, incUnread);
+ return roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0;
};
-const incUserMentions = async (
- rid: IRoom['_id'],
- roomType: RoomType,
- uids: IUser['_id'][],
- unreadCount: Exclude,
-): Promise => {
- const incUnreadByUser = new Set(['all_messages', 'user_mentions_only', 'user_and_group_mentions_only']).has(unreadCount);
- const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0;
- await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(rid, uids, 1, incUnread);
+const getUserMentions = (roomType: RoomType, unreadCount: Exclude): number => {
+ const incUnreadByUser = ['all_messages', 'user_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount);
+ return roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0;
};
export const getUserIdsFromHighlights = async (rid: IRoom['_id'], message: IMessage): Promise => {
@@ -100,53 +94,87 @@ const getUnreadSettingCount = (roomType: RoomType): UnreadCountType => {
};
async function updateUsersSubscriptions(message: IMessage, room: IRoom): Promise {
- // Don't increase unread counter on thread messages
- if (room != null && !message.tmid) {
- const { toAll, toHere, mentionIds } = await getMentions(message);
-
- const userIds = new Set(mentionIds);
-
- const unreadCount = getUnreadSettingCount(room.t);
+ if (!room || message.tmid) {
+ return;
+ }
- (await getUserIdsFromHighlights(room._id, message)).forEach((uid) => userIds.add(uid));
+ const [mentions, highlightIds] = await Promise.all([getMentions(message), getUserIdsFromHighlights(room._id, message)]);
+
+ const { toAll, toHere, mentionIds } = mentions;
+ const userIds = [...new Set([...mentionIds, ...highlightIds])];
+ const unreadCount = getUnreadSettingCount(room.t);
+
+ const userMentionInc = getUserMentions(room.t, unreadCount as Exclude);
+ const groupMentionInc = getGroupMentions(room.t, unreadCount as Exclude);
+
+ void Subscriptions.findByRoomIdAndNotAlertOrOpenExcludingUserIds({
+ roomId: room._id,
+ uidsExclude: [message.u._id],
+ uidsInclude: userIds,
+ onlyRead: !toAll && !toHere,
+ }).forEach((sub) => {
+ const hasUserMention = userIds.includes(sub.u._id);
+ const shouldIncUnread = hasUserMention || toAll || toHere || unreadCount === 'all_messages';
+ void notifyOnSubscriptionChanged(
+ {
+ ...sub,
+ alert: true,
+ open: true,
+ ...(shouldIncUnread && { unread: sub.unread + 1 }),
+ ...(hasUserMention && { userMentions: sub.userMentions + 1 }),
+ ...((toAll || toHere) && { groupMentions: sub.groupMentions + 1 }),
+ },
+ 'updated',
+ );
+ });
- // Give priority to user mentions over group mentions
- if (userIds.size > 0) {
- await incUserMentions(room._id, room.t, [...userIds], unreadCount as Exclude);
- } else if (toAll || toHere) {
- await incGroupMentions(room._id, room.t, message.u._id, unreadCount as Exclude);
- }
+ // Give priority to user mentions over group mentions
+ if (userIds.length) {
+ await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(room._id, userIds, 1, userMentionInc);
+ } else if (toAll || toHere) {
+ await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(room._id, message.u._id, 1, groupMentionInc);
+ }
- // this shouldn't run only if has group mentions because it will already exclude mentioned users from the query
- if (!toAll && !toHere && unreadCount === 'all_messages') {
- await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1);
- }
+ if (!toAll && !toHere && unreadCount === 'all_messages') {
+ await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1);
}
- // Update all other subscriptions to alert their owners but without incrementing
- // the unread counter, as it is only for mentions and direct messages
- // We now set alert and open properties in two separate update commands. This proved to be more efficient on MongoDB - because it uses a more efficient index.
+ // update subscriptions of other members of the room
await Promise.all([
Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id),
Subscriptions.setOpenForRoomIdExcludingUserId(message.rid, message.u._id),
]);
+
+ // update subscription of the message sender
+ await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id);
+ const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id);
+ if (setAsReadResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(message.rid, message.u._id);
+ }
}
export async function updateThreadUsersSubscriptions(message: IMessage, replies: IUser['_id'][]): Promise {
// Don't increase unread counter on thread messages
-
- await Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies);
const repliesPlusSender = [...new Set([message.u._id, ...replies])];
- await Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender);
- await Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date());
+
+ const responses = await Promise.all([
+ Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies),
+ Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender),
+ Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date()),
+ ]);
+
+ responses.some((response) => response?.modifiedCount) &&
+ void notifyOnSubscriptionChangedByRoomIdAndUserIds(message.rid, repliesPlusSender);
}
-export async function notifyUsersOnMessage(message: IMessage, room: IRoom): Promise {
+export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomUpdater: Updater): Promise {
+ console.log('notifyUsersOnMessage function');
+
// Skips this callback if the message was edited and increments it if the edit was way in the past (aka imported)
if (isEditedMessage(message)) {
if (Math.abs(moment(message.editedAt).diff(Date.now())) > 60000) {
// TODO: Review as I am not sure how else to get around this as the incrementing of the msgs count shouldn't be in this callback
- await Rooms.incMsgCountById(message.rid, 1);
+ Rooms.getIncMsgCountUpdateQuery(1, roomUpdater);
return message;
}
@@ -156,25 +184,39 @@ export async function notifyUsersOnMessage(message: IMessage, room: IRoom): Prom
(!message.tmid || message.tshow) &&
(!room.lastMessage || room.lastMessage._id === message._id)
) {
- await Rooms.setLastMessageById(message.rid, message);
+ Rooms.getLastMessageUpdateQuery(message, roomUpdater);
}
return message;
}
if (message.ts && Math.abs(moment(message.ts).diff(Date.now())) > 60000) {
- await Rooms.incMsgCountById(message.rid, 1);
+ Rooms.getIncMsgCountUpdateQuery(1, roomUpdater);
return message;
}
// If message sent ONLY on a thread, skips the rest as it is done on a callback specific to threads
if (message.tmid && !message.tshow) {
- await Rooms.incMsgCountById(message.rid, 1);
+ Rooms.getIncMsgCountUpdateQuery(1, roomUpdater);
return message;
}
// Update all the room activity tracker fields
- await Rooms.incMsgCountAndSetLastMessageById(message.rid, 1, message.ts, settings.get('Store_Last_Message') ? message : undefined);
+ Rooms.setIncMsgCountAndSetLastMessageUpdateQuery(1, message, !!settings.get('Store_Last_Message'), roomUpdater);
+ await updateUsersSubscriptions(message, room);
+
+ return message;
+}
+
+export async function notifyUsersOnSystemMessage(message: IMessage, room: IRoom): Promise {
+ const roomUpdater = Rooms.getUpdater();
+ Rooms.setIncMsgCountAndSetLastMessageUpdateQuery(1, message, !!settings.get('Store_Last_Message'), roomUpdater);
+
+ if (roomUpdater.hasChanges()) {
+ await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater);
+ }
+
+ // TODO: Rewrite to use just needed calls from the function
await updateUsersSubscriptions(message, room);
return message;
@@ -182,7 +224,15 @@ export async function notifyUsersOnMessage(message: IMessage, room: IRoom): Prom
callbacks.add(
'afterSaveMessage',
- (message, room) => notifyUsersOnMessage(message, room),
+ async (message, { room, roomUpdater }) => {
+ if (!roomUpdater) {
+ return message;
+ }
+
+ await notifyUsersOnMessage(message, room, roomUpdater);
+
+ return message;
+ },
callbacks.priority.MEDIUM,
'notifyUsersOnMessage',
);
diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
index 49fcc0ea4725..94c25f476222 100644
--- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
+++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
@@ -406,7 +406,7 @@ settings.watch('Troubleshoot_Disable_Notifications', (value) => {
callbacks.add(
'afterSaveMessage',
- (message, room) => sendAllNotifications(message, room),
+ (message, { room }) => sendAllNotifications(message, room),
callbacks.priority.LOW,
'sendNotificationsOnMessage',
);
diff --git a/apps/meteor/app/lib/server/methods/blockUser.ts b/apps/meteor/app/lib/server/methods/blockUser.ts
index b967e35d7bc1..7fe6ec803dd1 100644
--- a/apps/meteor/app/lib/server/methods/blockUser.ts
+++ b/apps/meteor/app/lib/server/methods/blockUser.ts
@@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor';
import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
+import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -33,14 +34,22 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}
- const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId);
- const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked);
+ const [blockedUser, blockerUser] = await Promise.all([
+ Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }),
+ Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }),
+ ]);
- if (!subscription || !subscription2) {
+ if (!blockedUser || !blockerUser) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}
- await Subscriptions.setBlockedByRoomId(rid, blocked, userId);
+ const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId);
+
+ const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])];
+
+ if (listenerUsers.length) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers);
+ }
return true;
},
diff --git a/apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts b/apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts
new file mode 100644
index 000000000000..e32f2ab5d7af
--- /dev/null
+++ b/apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts
@@ -0,0 +1,80 @@
+import { Federation, FederationEE, Authorization } from '@rocket.chat/core-services';
+import type { ServerMethods } from '@rocket.chat/ddp-client';
+import { License } from '@rocket.chat/license';
+import { Meteor } from 'meteor/meteor';
+
+declare module '@rocket.chat/ddp-client' {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ interface ServerMethods {
+ checkFederationConfiguration(): Promise<{ message: string }>;
+ }
+}
+
+Meteor.methods({
+ async checkFederationConfiguration() {
+ const uid = Meteor.userId();
+
+ if (!uid) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'checkFederationConfiguration',
+ });
+ }
+
+ if (!(await Authorization.hasPermission(uid, 'view-privileged-setting'))) {
+ throw new Meteor.Error('error-not-allowed', 'Action not allowed', {
+ method: 'checkFederationConfiguration',
+ });
+ }
+
+ const errors: string[] = [];
+
+ const successes: string[] = [];
+
+ const service = License.hasValidLicense() ? FederationEE : Federation;
+
+ const status = await service.configurationStatus();
+
+ if (status.externalReachability.ok) {
+ successes.push('homeserver configuration looks good');
+ } else {
+ let err = 'external reachability could not be verified';
+
+ const { error } = status.externalReachability;
+ if (error) {
+ err += `, error: ${error}`;
+ }
+
+ errors.push(err);
+ }
+
+ const {
+ roundTrip: { durationMs: duration },
+ } = status.appservice;
+
+ if (status.appservice.ok) {
+ successes.push(`appservice configuration looks good, total round trip time to homeserver ${duration}ms`);
+ } else {
+ errors.push(`failed to verify appservice configuration: ${status.appservice.error}`);
+ }
+
+ if (errors.length) {
+ void service.markConfigurationInvalid();
+
+ if (successes.length) {
+ const message = ['Configuration could only be partially verified'].concat(successes).concat(errors).join(', ');
+
+ throw new Meteor.Error('error-invalid-configuration', message, { method: 'checkFederationConfiguration' });
+ }
+
+ throw new Meteor.Error('error-invalid-configuration', ['Invalid configuration'].concat(errors).join(', '), {
+ method: 'checkFederationConfiguration',
+ });
+ }
+
+ void service.markConfigurationValid();
+
+ return {
+ message: ['All configuration looks good'].concat(successes).join(', '),
+ };
+ },
+});
diff --git a/apps/meteor/app/lib/server/methods/unblockUser.ts b/apps/meteor/app/lib/server/methods/unblockUser.ts
index 2eec5a082109..7b4bc5660010 100644
--- a/apps/meteor/app/lib/server/methods/unblockUser.ts
+++ b/apps/meteor/app/lib/server/methods/unblockUser.ts
@@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
+import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener';
+
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
@@ -20,14 +22,22 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' });
}
- const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId);
- const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked);
+ const [blockedUser, blockerUser] = await Promise.all([
+ Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }),
+ Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }),
+ ]);
- if (!subscription || !subscription2) {
+ if (!blockedUser || !blockerUser) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}
- await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId);
+ const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId);
+
+ const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])];
+
+ if (listenerUsers.length) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers);
+ }
return true;
},
diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts
index aff5e563f666..8cebe563cd23 100644
--- a/apps/meteor/app/lib/server/methods/updateMessage.ts
+++ b/apps/meteor/app/lib/server/methods/updateMessage.ts
@@ -12,7 +12,11 @@ import { updateMessage } from '../functions/updateMessage';
const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content'];
-export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) {
+export async function executeUpdateMessage(
+ uid: IUser['_id'],
+ message: AtLeast,
+ previewUrls?: string[],
+) {
const originalMessage = await Messages.findOneById(message._id);
if (!originalMessage?._id) {
return;
@@ -26,8 +30,11 @@ export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast {
+ async (message, { room }) => {
// TODO: check if I need to test this 60 second rule.
// If the message was edited, or is older than 60 seconds (imported)
// the notifications will be skipped, so we can also skip this validation
diff --git a/apps/meteor/app/livechat/client/lib/chartHandler.ts b/apps/meteor/app/livechat/client/lib/chartHandler.ts
index 19c1a004ca22..da2d4be3735c 100644
--- a/apps/meteor/app/livechat/client/lib/chartHandler.ts
+++ b/apps/meteor/app/livechat/client/lib/chartHandler.ts
@@ -177,10 +177,9 @@ export const drawDoughnutChart = async (
chartContext: { destroy: () => void } | undefined,
dataLabels: string[],
dataPoints: number[],
-): Promise | void> => {
+): Promise => {
if (!chart) {
- console.error('No chart element');
- return;
+ throw new Error('No chart element');
}
if (chartContext) {
chartContext.destroy();
@@ -200,7 +199,7 @@ export const drawDoughnutChart = async (
],
},
options: doughnutChartConfiguration(title),
- });
+ }) as ChartType;
};
/**
@@ -209,12 +208,12 @@ export const drawDoughnutChart = async (
* @param {String} label [chart label]
* @param {Array(Double)} data [updated data]
*/
-export const updateChart = async (c: ChartType, label: string, data: { [x: string]: number }): Promise => {
+export const updateChart = async (c: ChartType, label: string, data: number[]): Promise => {
const chart = await c;
if (chart.data?.labels?.indexOf(label) === -1) {
// insert data
chart.data.labels.push(label);
- chart.data.datasets.forEach((dataset: { data: any[] }, idx: string | number) => {
+ chart.data.datasets.forEach((dataset: { data: any[] }, idx: number) => {
dataset.data.push(data[idx]);
});
} else {
@@ -224,7 +223,7 @@ export const updateChart = async (c: ChartType, label: string, data: { [x: strin
return;
}
- chart.data.datasets.forEach((dataset: { data: { [x: string]: any } }, idx: string | number) => {
+ chart.data.datasets.forEach((dataset: { data: { [x: string]: any } }, idx: number) => {
dataset.data[index] = data[idx];
});
}
diff --git a/apps/meteor/app/livechat/server/api/lib/agents.ts b/apps/meteor/app/livechat/server/api/lib/agents.ts
index 3bc5180c2f59..2dbcce8c7e2e 100644
--- a/apps/meteor/app/livechat/server/api/lib/agents.ts
+++ b/apps/meteor/app/livechat/server/api/lib/agents.ts
@@ -7,14 +7,8 @@ export async function findAgentDepartments({
}: {
enabledDepartmentsOnly?: boolean;
agentId: string;
-}): Promise<{ departments: ILivechatDepartmentAgents[] }> {
- if (enabledDepartmentsOnly) {
- return {
- departments: await LivechatDepartmentAgents.findActiveDepartmentsByAgentId(agentId).toArray(),
- };
- }
-
+}): Promise<{ departments: (ILivechatDepartmentAgents & { departmentName: string })[] }> {
return {
- departments: await LivechatDepartmentAgents.find({ agentId }).toArray(),
+ departments: await LivechatDepartmentAgents.findDepartmentsOfAgent(agentId, enabledDepartmentsOnly).toArray(),
};
}
diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts
index 617d255cb6cb..a922edd40899 100644
--- a/apps/meteor/app/livechat/server/api/lib/livechat.ts
+++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts
@@ -61,6 +61,10 @@ export function findGuest(token: string): Promise {
});
}
+export function findGuestWithoutActivity(token: string): Promise {
+ return LivechatVisitors.getVisitorByToken(token, { projection: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1 } });
+}
+
export async function findRoom(token: string, rid?: string): Promise {
const fields = {
t: 1,
diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts
index 4c3cad33c130..abc6163fe9c9 100644
--- a/apps/meteor/app/livechat/server/api/v1/agent.ts
+++ b/apps/meteor/app/livechat/server/api/v1/agent.ts
@@ -6,6 +6,7 @@ import { isGETAgentNextToken, isPOSTLivechatAgentStatusProps } from '@rocket.cha
import { API } from '../../../../api/server';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
+import { RoutingManager } from '../../lib/RoutingManager';
import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat';
API.v1.addRoute('livechat/agent.info/:rid/:token', {
@@ -48,7 +49,7 @@ API.v1.addRoute(
}
}
- const agentData = await LivechatTyped.getNextAgent(department);
+ const agentData = await RoutingManager.getNextAgent(department);
if (!agentData) {
throw new Error('agent-not-found');
}
diff --git a/apps/meteor/app/livechat/server/api/v1/config.ts b/apps/meteor/app/livechat/server/api/v1/config.ts
index 79a6132136c5..17a2945e75de 100644
--- a/apps/meteor/app/livechat/server/api/v1/config.ts
+++ b/apps/meteor/app/livechat/server/api/v1/config.ts
@@ -2,8 +2,9 @@ import { isGETLivechatConfigParams } from '@rocket.chat/rest-typings';
import mem from 'mem';
import { API } from '../../../../api/server';
+import { settings as serverSettings } from '../../../../settings/server';
import { Livechat } from '../../lib/LivechatTyped';
-import { settings, findOpenRoom, getExtraConfigInfo, findAgent } from '../lib/livechat';
+import { settings, findOpenRoom, getExtraConfigInfo, findAgent, findGuestWithoutActivity } from '../lib/livechat';
const cachedSettings = mem(settings, { maxAge: process.env.TEST_MODE === 'true' ? 1 : 1000, cacheKey: JSON.stringify });
@@ -12,7 +13,7 @@ API.v1.addRoute(
{ validateParams: isGETLivechatConfigParams },
{
async get() {
- const enabled = Livechat.enabled();
+ const enabled = serverSettings.get('Livechat_enabled');
if (!enabled) {
return API.v1.success({ config: { enabled: false } });
@@ -23,7 +24,7 @@ API.v1.addRoute(
const config = await cachedSettings({ businessUnit });
const status = await Livechat.online(department);
- const guest = token ? await Livechat.findGuest(token) : null;
+ const guest = token ? await findGuestWithoutActivity(token) : null;
const room = guest ? await findOpenRoom(guest.token) : undefined;
const agent = guest && room && room.servedBy && (await findAgent(room.servedBy._id));
diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts
index 57c1d117f1b0..91b18a6b21af 100644
--- a/apps/meteor/app/livechat/server/api/v1/contact.ts
+++ b/apps/meteor/app/livechat/server/api/v1/contact.ts
@@ -1,14 +1,18 @@
import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models';
+import { isPOSTOmnichannelContactsProps } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { API } from '../../../../api/server';
-import { Contacts } from '../../lib/Contacts';
+import { Contacts, createContact } from '../../lib/Contacts';
API.v1.addRoute(
'omnichannel/contact',
- { authRequired: true, permissionsRequired: ['view-l-room'] },
+ {
+ authRequired: true,
+ permissionsRequired: ['view-l-room'],
+ },
{
async post() {
check(this.bodyParams, {
@@ -82,3 +86,18 @@ API.v1.addRoute(
},
},
);
+
+API.v1.addRoute(
+ 'omnichannel/contacts',
+ { authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps },
+ {
+ async post() {
+ if (!process.env.TEST_MODE) {
+ throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
+ }
+ const contactId = await createContact({ ...this.bodyParams, unknown: false });
+
+ return API.v1.success({ contactId });
+ },
+ },
+);
diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts
index e89a3e17f0a1..2688ad673af0 100644
--- a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts
+++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts
@@ -1,5 +1,4 @@
import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings';
-import { Messages } from '@rocket.chat/models';
import { isPOSTLivechatPageVisitedParams } from '@rocket.chat/rest-typings';
import { API } from '../../../../api/server';
@@ -11,17 +10,13 @@ API.v1.addRoute(
{
async post() {
const { token, rid, pageInfo } = this.bodyParams;
- const msgId = await Livechat.savePageHistory(token, rid, pageInfo);
- if (!msgId) {
- return API.v1.success();
- }
- const message = await Messages.findOneById(msgId);
+ const message = await Livechat.savePageHistory(token, rid, pageInfo);
if (!message) {
return API.v1.success();
}
- const { msg, navigation } = message;
+ const { msg, navigation } = message as IOmnichannelSystemMessage;
return API.v1.success({ page: { msg, navigation } });
},
},
diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.ts b/apps/meteor/app/livechat/server/api/v1/transcript.ts
index b36873a6ac27..e46e841628f1 100644
--- a/apps/meteor/app/livechat/server/api/v1/transcript.ts
+++ b/apps/meteor/app/livechat/server/api/v1/transcript.ts
@@ -6,6 +6,7 @@ import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams }
import { i18n } from '../../../../../server/lib/i18n';
import { API } from '../../../../api/server';
import { Livechat } from '../../lib/LivechatTyped';
+import { sendTranscript } from '../../lib/sendTranscript';
API.v1.addRoute(
'livechat/transcript',
@@ -13,7 +14,7 @@ API.v1.addRoute(
{
async post() {
const { token, rid, email } = this.bodyParams;
- if (!(await Livechat.sendTranscript({ token, rid, email }))) {
+ if (!(await sendTranscript({ token, rid, email }))) {
return API.v1.failure({ message: i18n.t('Error_sending_livechat_transcript') });
}
diff --git a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts
index 372704d339bb..311343c4ad01 100644
--- a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts
+++ b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts
@@ -5,7 +5,7 @@ import { callbacks } from '../../../../lib/callbacks';
callbacks.add(
'afterSaveMessage',
- async (message, room) => {
+ async (message, { room }) => {
if (!isOmnichannelRoom(room)) {
return message;
}
@@ -14,7 +14,7 @@ callbacks.add(
const result = await callbacks.run('afterOmnichannelSaveMessage', message, { room, roomUpdater: updater });
if (updater.hasChanges()) {
- await updater.persist({ _id: room._id });
+ await LivechatRooms.updateFromUpdater({ _id: room._id }, updater);
}
return result;
diff --git a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts
index 23131cee60a2..01d3014f1c27 100644
--- a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts
+++ b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts
@@ -5,7 +5,7 @@ import { callbacks } from '../../../../lib/callbacks';
callbacks.add(
'afterOmnichannelSaveMessage',
- async (message, { room }) => {
+ (message, { room, roomUpdater }) => {
// skips this callback if the message was edited
if (!message || isEditedMessage(message)) {
return message;
@@ -21,7 +21,7 @@ callbacks.add(
return message;
}
- await LivechatRooms.setNotResponseByRoomId(room._id);
+ LivechatRooms.getNotResponseByRoomIdUpdateQuery(roomUpdater);
return message;
},
diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
index 3e9164554d47..6820bd4664bd 100644
--- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
+++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
@@ -1,74 +1,72 @@
-import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
-import { isEditedMessage } from '@rocket.chat/core-typings';
+import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings';
+import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
+import type { Updater } from '@rocket.chat/models';
import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/models';
import moment from 'moment';
import { callbacks } from '../../../../lib/callbacks';
import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener';
-callbacks.add(
- 'afterOmnichannelSaveMessage',
- async (message, { room }) => {
- // skips this callback if the message was edited
- if (!message || isEditedMessage(message)) {
- return message;
- }
+export async function markRoomResponded(
+ message: IMessage,
+ room: IOmnichannelRoom,
+ roomUpdater: Updater,
+): Promise {
+ if (isSystemMessage(message) || isEditedMessage(message) || isMessageFromVisitor(message)) {
+ return;
+ }
- // skips this callback if the message is a system message
- if (message.t) {
- return message;
- }
+ const monthYear = moment().format('YYYY-MM');
+ const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);
- // if the message has a token, it was sent by the visitor, so ignore it
- if (message.token) {
- return message;
- }
+ // Case: agent answers & visitor is not active, we mark visitor as active
+ if (!isVisitorActive) {
+ await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
+ }
- // Return YYYY-MM from moment
- const monthYear = moment().format('YYYY-MM');
- const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);
+ if (!room.v?.activity?.includes(monthYear)) {
+ LivechatRooms.getVisitorActiveForPeriodUpdateQuery(monthYear, roomUpdater);
+ const livechatInquiry = await LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear);
- // Case: agent answers & visitor is not active, we mark visitor as active
- if (!isVisitorActive) {
- await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
+ if (livechatInquiry) {
+ void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v });
}
+ }
- if (!room.v?.activity?.includes(monthYear)) {
- 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) {
+ LivechatRooms.getAgentLastMessageTsUpdateQuery(roomUpdater);
+ }
+ if (!room.waitingResponse) {
+ // case where agent sends second message or any subsequent message in a room before visitor responds to the first message
+ // in this case, we just need to update the lastMessageTs of the responseBy object
if (room.responseBy) {
- await LivechatRooms.setAgentLastMessageTs(room._id);
+ LivechatRooms.getAgentLastMessageTsUpdateQuery(roomUpdater);
}
- // check if room is yet awaiting for response from visitor
- if (!room.waitingResponse) {
- // case where agent sends second message or any subsequent message in a room before visitor responds to the first message
- // in this case, we just need to update the lastMessageTs of the responseBy object
- if (room.responseBy) {
- await LivechatRooms.setAgentLastMessageTs(room._id);
- }
- return message;
- }
+ return room.responseBy;
+ }
+
+ const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || {
+ _id: message.u._id,
+ username: message.u.username,
+ firstResponseTs: new Date(message.ts),
+ lastMessageTs: new Date(message.ts),
+ };
+
+ LivechatRooms.getResponseByRoomIdUpdateQuery(responseBy, roomUpdater);
- // This is the first message from agent after visitor had last responded
- const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || {
- _id: message.u._id,
- username: message.u.username,
- firstResponseTs: new Date(message.ts),
- lastMessageTs: new Date(message.ts),
- };
+ return responseBy;
+}
- // this unsets waitingResponse and sets responseBy object
- await LivechatRooms.setResponseByRoomId(room._id, responseBy);
+callbacks.add(
+ 'afterOmnichannelSaveMessage',
+ async (message, { room, roomUpdater }) => {
+ if (!message || isEditedMessage(message) || isMessageFromVisitor(message) || isSystemMessage(message)) {
+ return;
+ }
- return message;
+ await markRoomResponded(message, room, roomUpdater);
},
callbacks.priority.HIGH,
'markRoomResponded',
diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts
index 8a5a4c280670..a6031bd42efa 100644
--- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts
+++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts
@@ -6,11 +6,12 @@ import moment from 'moment';
import { callbacks } from '../../../../lib/callbacks';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
+import type { CloseRoomParams } from '../lib/localTypes';
-const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) =>
+export const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) =>
moment(new Date(room.closedAt || new Date())).diff(moment(new Date(agentLastMessage.ts)), 'seconds');
-const parseDays = (
+export const parseDays = (
acc: Record,
day: IBusinessHourWorkHour,
) => {
@@ -22,7 +23,7 @@ const parseDays = (
return acc;
};
-const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
+export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
if (!settings.get('Livechat_enable_business_hours')) {
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
}
@@ -43,65 +44,91 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas
officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {});
}
- if (!officeDays) {
+ // Empty object we assume invalid config
+ if (!officeDays || !Object.keys(officeDays).length) {
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
}
let totalSeconds = 0;
- const endOfConversation = moment(new Date(room.closedAt || new Date()));
- const startOfInactivity = moment(new Date(agentLastMessage.ts));
+ const endOfConversation = moment.utc(new Date(room.closedAt || new Date()));
+ const startOfInactivity = moment.utc(new Date(agentLastMessage.ts));
const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days');
- const inactivityDay = moment(new Date(agentLastMessage.ts));
+ const inactivityDay = moment.utc(new Date(agentLastMessage.ts));
+
for (let index = 0; index <= daysOfInactivity; index++) {
const today = inactivityDay.clone().format('dddd');
const officeDay = officeDays[today];
- const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days');
- const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days');
- if (officeDays[today].open) {
- const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D');
- const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D');
-
- if (!firstDayOfInactivity && !lastDayOfInactivity) {
- totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds');
- } else {
- const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour;
- const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour;
- totalSeconds += end.clone().diff(start, 'seconds');
- }
+ if (!officeDay) {
+ inactivityDay.add(1, 'days');
+ continue;
+ }
+ if (!officeDay.open) {
+ inactivityDay.add(1, 'days');
+ continue;
+ }
+ if (!officeDay?.start?.time || !officeDay?.finish?.time) {
+ inactivityDay.add(1, 'days');
+ continue;
}
- inactivityDay.add(1, 'days');
- }
- return totalSeconds;
-};
-callbacks.add(
- 'livechat.closeRoom',
- async (params) => {
- const { room } = params;
+ const [officeStartHour, officeStartMinute] = officeDay.start.time.split(':');
+ const [officeCloseHour, officeCloseMinute] = officeDay.finish.time.split(':');
+ // We should only take in consideration the time where the office is open and the conversation was inactive
+ const todayStartOfficeHours = inactivityDay
+ .clone()
+ .set({ hour: parseInt(officeStartHour, 10), minute: parseInt(officeStartMinute, 10) });
+ const todayEndOfficeHours = inactivityDay.clone().set({ hour: parseInt(officeCloseHour, 10), minute: parseInt(officeCloseMinute, 10) });
- if (!isOmnichannelRoom(room)) {
- return params;
+ // 1: Room was inactive the whole day, we add the whole time BH is inactive
+ if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
+ totalSeconds += todayEndOfficeHours.diff(todayStartOfficeHours, 'seconds');
}
- const closedByAgent = room.closer !== 'visitor';
- const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
- if (!closedByAgent || !wasTheLastMessageSentByAgent) {
- return params;
+ // 2: Room was inactive before start but was closed before end of BH, we add the inactive time
+ if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
+ totalSeconds += endOfConversation.diff(todayStartOfficeHours, 'seconds');
}
- if (!room.v?.lastMessageTs) {
- return params;
+ // 3: Room was inactive after start and ended after end of BH, we add the inactive time
+ if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
+ totalSeconds += todayEndOfficeHours.diff(startOfInactivity, 'seconds');
}
- const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs);
- if (!agentLastMessage) {
- return params;
+ // 4: Room was inactive after start and before end of BH, we add the inactive time
+ if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
+ totalSeconds += endOfConversation.diff(startOfInactivity, 'seconds');
}
- const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
- await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);
+ inactivityDay.add(1, 'days');
+ }
+ return totalSeconds;
+};
+
+export const onCloseRoom = async (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => {
+ const { room } = params;
+
+ if (!isOmnichannelRoom(room)) {
return params;
- },
- callbacks.priority.HIGH,
- 'process-room-abandonment',
-);
+ }
+
+ const closedByAgent = room.closer !== 'visitor';
+ const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
+ if (!closedByAgent || !wasTheLastMessageSentByAgent) {
+ return params;
+ }
+
+ if (!room.v?.lastMessageTs) {
+ return params;
+ }
+
+ const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs);
+ if (!agentLastMessage) {
+ return params;
+ }
+ const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
+ await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);
+
+ return params;
+};
+
+callbacks.add('livechat.closeRoom', onCloseRoom, callbacks.priority.HIGH, 'process-room-abandonment');
diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
index fef6ad0936f8..9553e9fe981b 100644
--- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
+++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
@@ -1,4 +1,4 @@
-import { isEditedMessage } from '@rocket.chat/core-typings';
+import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
@@ -62,7 +62,7 @@ const getAnalyticsData = (room: IOmnichannelRoom, now: Date): Record {
- if (!message || isEditedMessage(message)) {
+ if (!message || isEditedMessage(message) || isSystemMessage(message)) {
return message;
}
@@ -70,8 +70,12 @@ callbacks.add(
message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } };
}
- const analyticsData = getAnalyticsData(room, new Date());
- await LivechatRooms.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData, roomUpdater);
+ if (isMessageFromVisitor(message)) {
+ LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater);
+ } else {
+ const analyticsData = getAnalyticsData(room, new Date());
+ LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater);
+ }
return message;
},
diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts
index 6f42a910417d..9969f03bf8bb 100644
--- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts
+++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts
@@ -1,7 +1,7 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
+import { LivechatVisitors } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
-import { Livechat } from '../lib/LivechatTyped';
callbacks.add(
'livechat.newRoom',
@@ -19,7 +19,7 @@ callbacks.add(
_id,
ts: new Date(),
};
- await Livechat.updateLastChat(guestId, lastChat);
+ await LivechatVisitors.setLastChatById(guestId, lastChat);
},
callbacks.priority.MEDIUM,
'livechat-save-last-chat',
diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts
index fcc0e0228bc7..f6a35f4dd7f9 100644
--- a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts
+++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts
@@ -3,8 +3,8 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
-import { Livechat } from '../lib/LivechatTyped';
import type { CloseRoomParams } from '../lib/LivechatTyped';
+import { sendTranscript } from '../lib/sendTranscript';
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;
@@ -30,10 +30,7 @@ const sendEmailTranscriptOnClose = async (params: LivechatCloseCallbackParams):
const { email, subject, requestedBy: user } = transcriptData;
- await Promise.all([
- Livechat.sendTranscript({ token, rid, email, subject, user }),
- LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid),
- ]);
+ await Promise.all([sendTranscript({ token, rid, email, subject, user }), LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid)]);
delete room.transcriptRequest;
diff --git a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts
index 3b7c6a3051bf..c0be707ba212 100644
--- a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts
@@ -1,7 +1,10 @@
import { OmnichannelAnalytics } from '@rocket.chat/core-services';
import mem from 'mem';
-export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify });
+export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, {
+ maxAge: process.env.TEST_MODE === 'true' ? 1 : 60000,
+ cacheKey: JSON.stringify,
+});
// Agent overview data on realtime is cached for 5 seconds
// while the data on the overview page is cached for 1 minute
export const getAnalyticsOverviewDataCached = mem(OmnichannelAnalytics.getAnalyticsOverviewData, {
diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts
index 2e648b02f5dd..4f4a33ee61b2 100644
--- a/apps/meteor/app/livechat/server/lib/Contacts.ts
+++ b/apps/meteor/app/livechat/server/lib/Contacts.ts
@@ -1,12 +1,25 @@
-import type { ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
-import { LivechatVisitors, Users, LivechatRooms, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models';
+import type { ILivechatContactChannel, ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
+import {
+ LivechatVisitors,
+ Users,
+ LivechatRooms,
+ LivechatCustomField,
+ LivechatInquiry,
+ Rooms,
+ Subscriptions,
+ LivechatContacts,
+} from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
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 {
+ notifyOnRoomChangedById,
+ notifyOnSubscriptionChangedByRoomId,
+ notifyOnLivechatInquiryChangedByRoom,
+} from '../../../lib/server/lib/notifyListener';
import { i18n } from '../../../utils/lib/i18n';
type RegisterContactProps = {
@@ -22,6 +35,16 @@ type RegisterContactProps = {
};
};
+type CreateContactParams = {
+ name: string;
+ emails: string[];
+ phones: string[];
+ unknown: boolean;
+ customFields?: Record;
+ contactManager?: string;
+ channels?: ILivechatContactChannel[];
+};
+
export const Contacts = {
async registerContact({
token,
@@ -138,17 +161,88 @@ export const Contacts = {
for await (const room of rooms) {
const { _id: rid } = room;
- await Promise.all([
+ const responses = await Promise.all([
Rooms.setFnameById(rid, name),
LivechatInquiry.setNameByRoomId(rid, name),
Subscriptions.updateDisplayNameByRoomId(rid, name),
]);
- void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name });
- void notifyOnRoomChangedById(rid);
+ if (responses[0]?.modifiedCount) {
+ void notifyOnRoomChangedById(rid);
+ }
+
+ if (responses[1]?.modifiedCount) {
+ void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name });
+ }
+
+ if (responses[2]?.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
}
}
return contactId;
},
};
+
+export async function createContact(params: CreateContactParams): Promise {
+ const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params;
+
+ if (contactManager) {
+ const contactManagerUser = await Users.findOneAgentById>(contactManager, { projection: { roles: 1 } });
+ if (!contactManagerUser) {
+ throw new Error('error-contact-manager-not-found');
+ }
+ }
+
+ const allowedCustomFields = await getAllowedCustomFields();
+ validateCustomFields(allowedCustomFields, customFields);
+
+ const { insertedId } = await LivechatContacts.insertOne({
+ name,
+ emails,
+ phones,
+ contactManager,
+ channels,
+ customFields,
+ unknown,
+ });
+
+ return insertedId;
+}
+
+async function getAllowedCustomFields(): Promise {
+ return LivechatCustomField.findByScope(
+ 'visitor',
+ {
+ projection: { _id: 1, label: 1, regexp: 1, required: 1 },
+ },
+ false,
+ ).toArray();
+}
+
+export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record) {
+ for (const cf of allowedCustomFields) {
+ if (!customFields.hasOwnProperty(cf._id)) {
+ if (cf.required) {
+ throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
+ }
+ continue;
+ }
+ const cfValue: string = trim(customFields[cf._id]);
+
+ if (!cfValue || typeof cfValue !== 'string') {
+ if (cf.required) {
+ throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
+ }
+ continue;
+ }
+
+ if (cf.regexp) {
+ const regex = new RegExp(cf.regexp);
+ if (!regex.test(cfValue)) {
+ throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
+ }
+ }
+ }
+}
diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts
index c0e85a8c7c2b..17f21d8d7b04 100644
--- a/apps/meteor/app/livechat/server/lib/Helper.ts
+++ b/apps/meteor/app/livechat/server/lib/Helper.ts
@@ -36,10 +36,12 @@ import { validateEmail as validatorFunc } from '../../../../lib/emailValidator';
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,
+ notifyOnSubscriptionChangedById,
+ notifyOnSubscriptionChangedByRoomId,
+ notifyOnSubscriptionChanged,
} from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { Livechat as LivechatTyped } from './LivechatTyped';
@@ -141,10 +143,7 @@ export const createLivechatRoom = async <
}
await callbacks.run('livechat.newRoom', room);
-
- // TODO: replace with `Message.saveSystemMessage`
-
- await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false, token: guest.token }, room);
+ await Message.saveSystemMessageAndNotifyUser('livechat-started', rid, '', { _id, username }, { groupable: false, token: guest.token });
return result.value as IOmnichannelRoom;
};
@@ -289,7 +288,13 @@ export const createLivechatSubscription = async (
...(department && { department }),
} as InsertionModel;
- return Subscriptions.insertOne(subscriptionData);
+ const response = await Subscriptions.insertOne(subscriptionData);
+
+ if (response?.insertedId) {
+ void notifyOnSubscriptionChangedById(response.insertedId, 'inserted');
+ }
+
+ return response;
};
export const removeAgentFromSubscription = async (rid: string, { _id, username }: Pick) => {
@@ -300,7 +305,11 @@ export const removeAgentFromSubscription = async (rid: string, { _id, username }
return;
}
- await Subscriptions.removeByRoomIdAndUserId(rid, _id);
+ const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, _id);
+ if (deletedSubscription) {
+ void notifyOnSubscriptionChanged(deletedSubscription, 'removed');
+ }
+
await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name });
setImmediate(() => {
@@ -517,12 +526,16 @@ export const updateChatDepartment = async ({
newDepartmentId: string;
oldDepartmentId?: string;
}) => {
- await Promise.all([
+ const responses = await Promise.all([
LivechatRooms.changeDepartmentIdByRoomId(rid, newDepartmentId),
LivechatInquiry.changeDepartmentIdByRoomId(rid, newDepartmentId),
Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId),
]);
+ if (responses[2].modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomId(rid);
+ }
+
setImmediate(() => {
void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, {
type: LivechatTransferEventType.DEPARTMENT,
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index ccca7a8eb68e..be79d565f6de 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -21,6 +21,7 @@ import type {
ILivechatDepartmentAgents,
LivechatDepartmentDTO,
OmnichannelSourceType,
+ ILivechatInquiryRecord,
} from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger, type MainLogger } from '@rocket.chat/logger';
@@ -40,11 +41,12 @@ import {
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
-import type { Filter, FindCursor } from 'mongodb';
+import type { Filter, FindCursor, ClientSession, MongoError } from 'mongodb';
import UAParser from 'ua-parser-js';
import { callbacks } from '../../../../lib/callbacks';
import { trim } from '../../../../lib/utils/stringUtils';
+import { client } from '../../../../server/database/utils';
import { i18n } from '../../../../server/lib/i18n';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles';
@@ -60,8 +62,10 @@ import {
notifyOnLivechatInquiryChangedByRoom,
notifyOnRoomChangedById,
notifyOnLivechatInquiryChangedByToken,
- notifyOnLivechatDepartmentAgentChangedByDepartmentId,
notifyOnUserChange,
+ notifyOnLivechatDepartmentAgentChangedByDepartmentId,
+ notifyOnSubscriptionChangedByRoomId,
+ notifyOnSubscriptionChanged,
} from '../../../lib/server/lib/notifyListener';
import * as Mailer from '../../../mailer/server/api';
import { metrics } from '../../../metrics/server';
@@ -73,7 +77,6 @@ import { RoutingManager } from './RoutingManager';
import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes';
import { parseTranscriptRequest } from './parseTranscriptRequest';
-import { sendTranscript as sendTranscriptFunc } from './sendTranscript';
type RegisterGuestType = Partial> & {
id?: string;
@@ -140,6 +143,13 @@ type ICRMData = {
crmData?: IOmnichannelRoom['crmData'];
};
+type ChatCloser = { _id: string; username: string | undefined };
+
+const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser =>
+ (params as CloseRoomParamsByUser).user !== undefined;
+const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor =>
+ (params as CloseRoomParamsByVisitor).visitor !== undefined;
+
const dnsResolveMx = util.promisify(dns.resolveMx);
class LivechatClass {
@@ -152,22 +162,6 @@ class LivechatClass {
this.webhookLogger = this.logger.section('Webhook');
}
- findGuest(token: string) {
- return LivechatVisitors.getVisitorByToken(token, {
- projection: {
- name: 1,
- username: 1,
- token: 1,
- visitorEmails: 1,
- department: 1,
- },
- });
- }
-
- enabled() {
- return Boolean(settings.get('Livechat_enabled'));
- }
-
async online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise {
Livechat.logger.debug(`Checking online agents ${department ? `for department ${department}` : ''}`);
if (!skipNoAgentSetting && settings.get('Livechat_accept_chats_with_no_agents')) {
@@ -192,10 +186,6 @@ class LivechatClass {
return agentsOnline;
}
- getNextAgent(department?: string): Promise {
- return RoutingManager.getNextAgent(department);
- }
-
async getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> {
if (agent?.agentId) {
return Users.findOnlineAgents(agent.agentId);
@@ -217,14 +207,115 @@ class LivechatClass {
return Users.findOnlineAgents();
}
- async closeRoom(params: CloseRoomParams): Promise {
+ async closeRoom(params: CloseRoomParams, attempts = 2): Promise {
+ let newRoom: IOmnichannelRoom;
+ let chatCloser: ChatCloser;
+ let removedInquiryObj: ILivechatInquiryRecord | null;
+
+ const session = client.startSession();
+ try {
+ session.startTransaction();
+ const { room, closedBy, removedInquiry } = await this.doCloseRoom(params, session);
+ await session.commitTransaction();
+
+ newRoom = room;
+ chatCloser = closedBy;
+ removedInquiryObj = removedInquiry;
+ } catch (e) {
+ this.logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts });
+ await session.abortTransaction();
+ // Dont propagate transaction errors
+ if (
+ (e as unknown as MongoError)?.errorLabels?.includes('UnknownTransactionCommitResult') ||
+ (e as unknown as MongoError)?.errorLabels?.includes('TransientTransactionError')
+ ) {
+ if (attempts > 0) {
+ this.logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`);
+ return this.closeRoom(params, attempts - 1);
+ }
+
+ throw new Error('error-room-cannot-be-closed-try-again');
+ }
+ throw e;
+ } finally {
+ await session.endSession();
+ }
+
+ // Note: when reaching this point, the room has been closed
+ // Transaction is commited and so these messages can be sent here.
+ return this.afterRoomClosed(newRoom, chatCloser, removedInquiryObj, params);
+ }
+
+ async afterRoomClosed(
+ newRoom: IOmnichannelRoom,
+ chatCloser: ChatCloser,
+ inquiry: ILivechatInquiryRecord | null,
+ params: CloseRoomParams,
+ ): Promise {
+ if (!chatCloser) {
+ // this should never happen
+ return;
+ }
+ // Note: we are okay with these messages being sent outside of the transaction. The process of sending a message
+ // is huge and involves multiple db calls. Making it transactionable this way would be really hard.
+ // And passing just _some_ actions to the transaction creates some deadlocks since messages are updated in the afterSaveMessages callbacks.
+ const transcriptRequested =
+ !!params.room.transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));
+ this.logger.debug(`Sending closing message to room ${newRoom._id}`);
+ await Message.saveSystemMessageAndNotifyUser('livechat-close', newRoom._id, params.comment ?? '', chatCloser, {
+ groupable: false,
+ transcriptRequested,
+ ...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }),
+ });
+
+ if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) {
+ await Message.saveSystemMessage('command', newRoom._id, 'promptTranscript', chatCloser);
+ }
+
+ this.logger.debug(`Running callbacks for room ${newRoom._id}`);
+
+ process.nextTick(() => {
+ /**
+ * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed
+ * in the next major version of the Apps-Engine
+ */
+ void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom);
+ void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom);
+ });
+
+ const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined;
+ const opts = await parseTranscriptRequest(params.room, params.options, visitor);
+ if (process.env.TEST_MODE) {
+ await callbacks.run('livechat.closeRoom', {
+ room: newRoom,
+ options: opts,
+ });
+ } else {
+ callbacks.runAsync('livechat.closeRoom', {
+ room: newRoom,
+ options: opts,
+ });
+ }
+
+ void notifyOnRoomChangedById(newRoom._id);
+ if (inquiry) {
+ void notifyOnLivechatInquiryChanged(inquiry, 'removed');
+ }
+
+ this.logger.debug(`Room ${newRoom._id} was closed`);
+ }
+
+ async doCloseRoom(
+ params: CloseRoomParams,
+ session: ClientSession,
+ ): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
const { comment } = params;
const { room } = params;
this.logger.debug(`Attempting to close room ${room._id}`);
if (!room || !isOmnichannelRoom(room) || !room.open) {
this.logger.debug(`Room ${room._id} is not open`);
- return;
+ throw new Error('error-room-closed');
}
const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation');
@@ -236,7 +327,7 @@ class LivechatClass {
this.logger.debug(`Resolved chat tags for room ${room._id}`);
const now = new Date();
- const { _id: rid, servedBy, transcriptRequest } = room;
+ const { _id: rid, servedBy } = room;
const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000;
const closeData: IOmnichannelRoomClosingInfo = {
@@ -247,12 +338,6 @@ class LivechatClass {
};
this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`);
- const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser =>
- (params as CloseRoomParamsByUser).user !== undefined;
- const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor =>
- (params as CloseRoomParamsByVisitor).visitor !== undefined;
-
- let chatCloser: any;
if (isRoomClosedByUserParams(params)) {
const { user } = params;
this.logger.debug(`Closing by user ${user?._id}`);
@@ -261,7 +346,6 @@ class LivechatClass {
_id: user?._id || '',
username: user?.username,
};
- chatCloser = user;
} else if (isRoomClosedByVisitorParams(params)) {
const { visitor } = params;
this.logger.debug(`Closing by visitor ${params.visitor._id}`);
@@ -270,87 +354,44 @@ class LivechatClass {
_id: visitor._id,
username: visitor.username,
};
- chatCloser = visitor;
} else {
throw new Error('Error: Please provide details of the user or visitor who closed the room');
}
this.logger.debug(`Updating DB for room ${room._id} with close data`);
- const inquiry = await LivechatInquiry.findOneByRoomId(rid);
-
- const removedInquiry = await LivechatInquiry.removeByRoomId(rid);
+ const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session });
+ const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session });
if (removedInquiry && removedInquiry.deletedCount !== 1) {
throw new Error('Error removing inquiry');
}
- if (inquiry) {
- void notifyOnLivechatInquiryChanged(inquiry, 'removed');
- }
- const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData);
+ const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session });
if (!updatedRoom || updatedRoom.modifiedCount !== 1) {
throw new Error('Error closing room');
}
- await Subscriptions.removeByRoomId(rid);
+ const subs = await Subscriptions.countByRoomId(rid, { session });
+ const removedSubs = await Subscriptions.removeByRoomId(rid, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ session,
+ });
- this.logger.debug(`DB updated for room ${room._id}`);
+ if (removedSubs.deletedCount !== subs) {
+ throw new Error('Error removing subscriptions');
+ }
- const transcriptRequested =
- !!transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));
+ this.logger.debug(`DB updated for room ${room._id}`);
// Retrieve the closed room
- const newRoom = await LivechatRooms.findOneById(rid);
-
+ const newRoom = await LivechatRooms.findOneById(rid, { session });
if (!newRoom) {
throw new Error('Error: Room not found');
}
- this.logger.debug(`Sending closing message to room ${room._id}`);
- await sendMessage(
- chatCloser,
- {
- t: 'livechat-close',
- msg: comment,
- groupable: false,
- transcriptRequested,
- ...(isRoomClosedByVisitorParams(params) && { token: chatCloser.token }),
- },
- newRoom,
- );
-
- if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) {
- await Message.saveSystemMessage('command', rid, 'promptTranscript', closeData.closedBy);
- }
-
- this.logger.debug(`Running callbacks for room ${newRoom._id}`);
-
- process.nextTick(() => {
- /**
- * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed
- * in the next major version of the Apps-Engine
- */
- void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom);
- void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom);
- });
-
- const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined;
- const opts = await parseTranscriptRequest(params.room, options, visitor);
- if (process.env.TEST_MODE) {
- await callbacks.run('livechat.closeRoom', {
- room: newRoom,
- options: opts,
- });
- } else {
- callbacks.runAsync('livechat.closeRoom', {
- room: newRoom,
- options: opts,
- });
- }
-
- void notifyOnRoomChangedById(newRoom._id);
-
- this.logger.debug(`Room ${newRoom._id} was closed`);
+ return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry };
}
async getRequiredDepartment(onlineRequired = true) {
@@ -389,7 +430,7 @@ class LivechatClass {
agent?: SelectedAgent;
extraData?: Record;
}) {
- if (!this.enabled()) {
+ if (!settings.get('Livechat_enabled')) {
throw new Meteor.Error('error-omnichannel-is-disabled');
}
@@ -440,7 +481,7 @@ class LivechatClass {
agent?: SelectedAgent,
extraData?: E,
) {
- if (!this.enabled()) {
+ if (!settings.get('Livechat_enabled')) {
throw new Meteor.Error('error-omnichannel-is-disabled');
}
Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`);
@@ -526,12 +567,16 @@ class LivechatClass {
const result = await Promise.allSettled([
Messages.removeByRoomId(rid),
ReadReceipts.removeByRoomId(rid),
- Subscriptions.removeByRoomId(rid),
+ Subscriptions.removeByRoomId(rid, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ }),
LivechatInquiry.removeByRoomId(rid),
LivechatRooms.removeById(rid),
]);
- if (inquiry) {
+ if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) {
void notifyOnLivechatInquiryChanged(inquiry, 'removed');
}
@@ -810,15 +855,6 @@ class LivechatClass {
}
}
- async updateLastChat(contactId: string, lastChat: Required) {
- const updateUser = {
- $set: {
- lastChat,
- },
- };
- await LivechatVisitors.updateById(contactId, updateUser);
- }
-
notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) {
void api.broadcast('omnichannel.room', roomId, {
type: 'visitorData',
@@ -878,7 +914,7 @@ class LivechatClass {
return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, {
sort: { ts: 1 },
- }).toArray();
+ });
}
async archiveDepartment(_id: string) {
@@ -1154,13 +1190,18 @@ class LivechatClass {
const cursor = LivechatRooms.findByVisitorToken(token);
for await (const room of cursor) {
await Promise.all([
+ Subscriptions.removeByRoomId(room._id, {
+ async onTrash(doc) {
+ void notifyOnSubscriptionChanged(doc, 'removed');
+ },
+ }),
FileUpload.removeFilesByRoomId(room._id),
Messages.removeByRoomId(room._id),
ReadReceipts.removeByRoomId(room._id),
]);
}
- await Promise.all([Subscriptions.removeByVisitorToken(token), LivechatRooms.removeByVisitorToken(token)]);
+ await LivechatRooms.removeByVisitorToken(token);
const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
@@ -1254,31 +1295,20 @@ class LivechatClass {
const scopeData = scope || (nextDepartment ? 'department' : 'agent');
this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`);
- await sendMessage(
- transferredBy,
- {
- t: 'livechat_transfer_history',
- rid: room._id,
+ const transferMessage = {
+ ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }),
+ transferData: {
+ transferredBy,
ts: new Date(),
- msg: '',
- u: {
- _id,
- username,
- },
- groupable: false,
- ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }),
- transferData: {
- transferredBy,
- ts: new Date(),
- scope: scopeData,
- comment,
- ...(previousDepartment && { previousDepartment }),
- ...(nextDepartment && { nextDepartment }),
- ...(transferredTo && { transferredTo }),
- },
+ scope: scopeData,
+ comment,
+ ...(previousDepartment && { previousDepartment }),
+ ...(nextDepartment && { nextDepartment }),
+ ...(transferredTo && { transferredTo }),
},
- room,
- );
+ };
+
+ await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage);
}
async saveGuest(guestData: Pick & { email?: string; phone?: string }, userId: string) {
@@ -1723,13 +1753,19 @@ class LivechatClass {
const { _id: rid } = roomData;
const { name } = guestData;
- await Promise.all([
+ const responses = await Promise.all([
Rooms.setFnameById(rid, name),
LivechatInquiry.setNameByRoomId(rid, name),
Subscriptions.updateDisplayNameByRoomId(rid, name),
]);
- void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name });
+ if (responses[1]?.modifiedCount) {
+ void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name });
+ }
+
+ if (responses[2]?.modifiedCount) {
+ await notifyOnSubscriptionChangedByRoomId(rid);
+ }
}
void notifyOnRoomChangedById(roomData._id);
@@ -1841,22 +1877,6 @@ class LivechatClass {
return departmentDB;
}
-
- async sendTranscript({
- token,
- rid,
- email,
- subject,
- user,
- }: {
- token: string;
- rid: string;
- email: string;
- subject?: string;
- user?: Pick | null;
- }): Promise {
- return sendTranscriptFunc({ token, rid, email, subject, user });
- }
}
export const Livechat = new LivechatClass();
diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts
index 2075a5e9af97..e1ea79d84163 100644
--- a/apps/meteor/app/livechat/server/lib/QueueManager.ts
+++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts
@@ -223,7 +223,7 @@ export class QueueManager {
const name = (roomInfo?.fname as string) || guest.name || guest.username;
- const room = await createLivechatRoom(rid, name, guest, roomInfo, {
+ const room = await createLivechatRoom(rid, name, { ...guest, ...(department && { department }) }, roomInfo, {
...extraData,
...(Boolean(customFields) && { customFields }),
});
diff --git a/apps/meteor/app/livechat/server/methods/getNextAgent.ts b/apps/meteor/app/livechat/server/methods/getNextAgent.ts
index 179f1f95dedf..f603aeef97f3 100644
--- a/apps/meteor/app/livechat/server/methods/getNextAgent.ts
+++ b/apps/meteor/app/livechat/server/methods/getNextAgent.ts
@@ -8,6 +8,7 @@ import { callbacks } from '../../../../lib/callbacks';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { settings } from '../../../settings/server';
import { Livechat } from '../lib/LivechatTyped';
+import { RoutingManager } from '../lib/RoutingManager';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -38,7 +39,7 @@ Meteor.methods({
}
}
- const agent = await Livechat.getNextAgent(department);
+ const agent = await RoutingManager.getNextAgent(department);
if (!agent) {
return;
}
diff --git a/apps/meteor/app/livechat/server/methods/sendTranscript.ts b/apps/meteor/app/livechat/server/methods/sendTranscript.ts
index 4891f579926a..00287fa89327 100644
--- a/apps/meteor/app/livechat/server/methods/sendTranscript.ts
+++ b/apps/meteor/app/livechat/server/methods/sendTranscript.ts
@@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { RateLimiter } from '../../../lib/server';
-import { Livechat } from '../lib/LivechatTyped';
+import { sendTranscript } from '../lib/sendTranscript';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -39,7 +39,7 @@ Meteor.methods({
throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:sendTranscript' });
}
- return Livechat.sendTranscript({ token, rid, email, subject, user });
+ return sendTranscript({ token, rid, email, subject, user });
},
});
diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts
index 6ef1f5567a20..f213ae4b7243 100644
--- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts
+++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts
@@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, Subscriptions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
+import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener';
import logger from './logger';
declare module '@rocket.chat/ddp-client' {
@@ -36,7 +37,11 @@ Meteor.methods({
});
}
- await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts);
+ const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts);
+ if (setAsUnreadResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(lastMessage.rid, userId);
+ }
+
return;
}
@@ -72,7 +77,11 @@ Meteor.methods({
if (firstUnreadMessage.ts >= lastSeen) {
return logger.debug('Provided message is already marked as unread');
}
- logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`);
- await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts);
+
+ logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`);
+ const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts);
+ if (setAsUnreadResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(originalMessage.rid, userId);
+ }
},
});
diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts
index f691a775cb6a..9f3dd44cc16d 100644
--- a/apps/meteor/app/message-pin/server/pinMessage.ts
+++ b/apps/meteor/app/message-pin/server/pinMessage.ts
@@ -134,7 +134,7 @@ Meteor.methods({
const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned';
- const msgId = await Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, {
+ return Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, {
attachments: [
{
text: originalMessage.msg,
@@ -145,8 +145,6 @@ Meteor.methods({
},
],
});
-
- return Messages.findOneById(msgId);
},
async unpinMessage(message) {
check(message._id, String);
diff --git a/apps/meteor/app/models/client/models/CachedChatRoom.ts b/apps/meteor/app/models/client/models/CachedChatRoom.ts
index f66e5b447432..852bed5a6067 100644
--- a/apps/meteor/app/models/client/models/CachedChatRoom.ts
+++ b/apps/meteor/app/models/client/models/CachedChatRoom.ts
@@ -46,7 +46,6 @@ class CachedChatRoom extends CachedCollection {
usernames: room.usernames,
usersCount: room.usersCount,
lastMessage: room.lastMessage,
- streamingOptions: room.streamingOptions,
teamId: room.teamId,
teamMain: room.teamMain,
v: (room as IOmnichannelRoom | undefined)?.v,
diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/app/models/client/models/CachedChatSubscription.ts
index 0e325453539a..7c0e84800c77 100644
--- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts
+++ b/apps/meteor/app/models/client/models/CachedChatSubscription.ts
@@ -35,7 +35,6 @@ class CachedChatSubscription extends CachedCollection
@@ -132,7 +133,10 @@ Meteor.methods({
});
}
- await notifications[field].updateMethod(subscription, value);
+ const updateResponse = await notifications[field].updateMethod(subscription, value);
+ if (updateResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedById(subscription._id);
+ }
return true;
},
@@ -144,13 +148,19 @@ Meteor.methods({
method: 'saveAudioNotificationValue',
});
}
+
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId);
if (!subscription) {
throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', {
method: 'saveAudioNotificationValue',
});
}
- await saveAudioNotificationValue(subscription._id, value);
+
+ const saveAudioNotificationResponse = await saveAudioNotificationValue(subscription._id, value);
+ if (saveAudioNotificationResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedById(subscription._id);
+ }
+
return true;
},
});
diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts
index e35103e9d333..d513c8dda6a5 100644
--- a/apps/meteor/app/reactions/server/setReaction.ts
+++ b/apps/meteor/app/reactions/server/setReaction.ts
@@ -1,5 +1,5 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
-import { api } from '@rocket.chat/core-services';
+import { api, Message } from '@rocket.chat/core-services';
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models';
@@ -52,6 +52,8 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction
// return;
// }
+ await Message.beforeReacted(message, room);
+
const userAlreadyReacted =
message.reactions &&
Boolean(message.reactions[reaction]) &&
diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts
index 337691bfbe57..640aa517a679 100644
--- a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts
+++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts
@@ -6,13 +6,20 @@ import { getCronAdvancedTimerFromPrecisionSetting } from '../../../lib/getCronAd
import { cleanRoomHistory } from '../../lib/server/functions/cleanRoomHistory';
import { settings } from '../../settings/server';
-const maxTimes = {
- c: 0,
- p: 0,
- d: 0,
+type RetentionRoomTypes = 'c' | 'p' | 'd';
+
+const getMaxAgeSettingIdByRoomType = (type: RetentionRoomTypes) => {
+ switch (type) {
+ case 'c':
+ return settings.get('RetentionPolicy_TTL_Channels');
+ case 'p':
+ return settings.get('RetentionPolicy_TTL_Groups');
+ case 'd':
+ return settings.get('RetentionPolicy_TTL_DMs');
+ }
};
-let types: (keyof typeof maxTimes)[] = [];
+let types: RetentionRoomTypes[] = [];
const oldest = new Date('0001-01-01T00:00:00Z');
@@ -29,7 +36,7 @@ async function job(): Promise {
// get all rooms with default values
for await (const type of types) {
- const maxAge = maxTimes[type] || 0;
+ const maxAge = getMaxAgeSettingIdByRoomType(type) || 0;
const latest = new Date(now.getTime() - maxAge);
const rooms = await Rooms.find(
@@ -95,9 +102,6 @@ settings.watchMultiple(
'RetentionPolicy_AppliesToChannels',
'RetentionPolicy_AppliesToGroups',
'RetentionPolicy_AppliesToDMs',
- 'RetentionPolicy_TTL_Channels',
- 'RetentionPolicy_TTL_Groups',
- 'RetentionPolicy_TTL_DMs',
'RetentionPolicy_Advanced_Precision',
'RetentionPolicy_Advanced_Precision_Cron',
'RetentionPolicy_Precision',
@@ -120,10 +124,6 @@ settings.watchMultiple(
types.push('d');
}
- 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')) ||
getCronAdvancedTimerFromPrecisionSetting(settings.get('RetentionPolicy_Precision'));
diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts
index e86a391ad8fa..d7d2fa0a79f8 100644
--- a/apps/meteor/app/settings/server/SettingsRegistry.ts
+++ b/apps/meteor/app/settings/server/SettingsRegistry.ts
@@ -139,6 +139,7 @@ export class SettingsRegistry {
const settingFromCodeOverwritten = overwriteSetting(settingFromCode);
const settingStored = this.store.getSetting(_id);
+
const settingStoredOverwritten = settingStored && overwriteSetting(settingStored);
try {
@@ -166,7 +167,10 @@ export class SettingsRegistry {
})();
await this.saveUpdatedSetting(_id, updatedProps, removedKeys);
- this.store.set(settingFromCodeOverwritten);
+ if ('value' in updatedProps) {
+ this.store.set(updatedProps as ISetting);
+ }
+
return;
}
diff --git a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js
index 6aeffb7bef45..eebc07ddb72d 100644
--- a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js
+++ b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js
@@ -1,5 +1,5 @@
import { settings } from '../../settings/client';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
settings.onload('SlackBridge_Enabled', (key, value) => {
if (value) {
diff --git a/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts
index 4c9d6a4e40c8..7cd5edb6bb87 100644
--- a/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/client/gimme.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
/*
* Gimme is a named function that will replace /gimme commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts
index 99eaa03b9e59..0e3cc55f6b86 100644
--- a/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/client/lenny.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
/*
* Lenny is a named function that will replace /lenny commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts
index bc0fb300789e..c4bdec8f1a8c 100644
--- a/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/client/shrug.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
/*
* Shrug is a named function that will replace /shrug commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts
index 0d709760fe84..8820b81f7c4a 100644
--- a/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
/*
* Tableflip is a named function that will replace /Tableflip commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts
index a7dc0d257e78..6c02fa196052 100644
--- a/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/client/unflip.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
/*
* Unflip is a named function that will replace /unflip commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts
index f426d6cf85c0..f902d75f33d1 100644
--- a/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/server/gimme.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Gimme is a named function that will replace /gimme commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts
index 878a10e356d4..e760b5a1169e 100644
--- a/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/server/lenny.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Lenny is a named function that will replace /lenny commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts
index 1240027bb38f..c2e5d166bfd8 100644
--- a/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/server/shrug.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Shrug is a named function that will replace /shrug commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts
index 34acef9805e2..ac3f599dff1d 100644
--- a/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/server/tableflip.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Tableflip is a named function that will replace /Tableflip commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts
index 689e7262eac0..b905ed567447 100644
--- a/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts
+++ b/apps/meteor/app/slashcommand-asciiarts/server/unflip.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Unflip is a named function that will replace /unflip commands
* @param {Object} message - The message object
diff --git a/apps/meteor/app/slashcommands-archiveroom/client/client.ts b/apps/meteor/app/slashcommands-archiveroom/client/client.ts
index c24763106684..f5154fb32a5b 100644
--- a/apps/meteor/app/slashcommands-archiveroom/client/client.ts
+++ b/apps/meteor/app/slashcommands-archiveroom/client/client.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'archive',
diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts
index 99bcec2cd7b3..f1b33c1022bb 100644
--- a/apps/meteor/app/slashcommands-archiveroom/server/server.ts
+++ b/apps/meteor/app/slashcommands-archiveroom/server/server.ts
@@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator';
import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission';
import { archiveRoom } from '../../lib/server/functions/archiveRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'archive',
diff --git a/apps/meteor/app/slashcommands-create/client/client.ts b/apps/meteor/app/slashcommands-create/client/client.ts
index 299db606db9c..7e8ba831dbd8 100644
--- a/apps/meteor/app/slashcommands-create/client/client.ts
+++ b/apps/meteor/app/slashcommands-create/client/client.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'create',
diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts
index 104d50c56926..6abee71c56fd 100644
--- a/apps/meteor/app/slashcommands-create/server/server.ts
+++ b/apps/meteor/app/slashcommands-create/server/server.ts
@@ -6,7 +6,7 @@ import { i18n } from '../../../server/lib/i18n';
import { createChannelMethod } from '../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'create',
diff --git a/apps/meteor/app/slashcommands-help/server/server.ts b/apps/meteor/app/slashcommands-help/server/server.ts
index c24bfb22c6fe..80efaffeb852 100644
--- a/apps/meteor/app/slashcommands-help/server/server.ts
+++ b/apps/meteor/app/slashcommands-help/server/server.ts
@@ -4,7 +4,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Help is a named function that will replace /help commands
diff --git a/apps/meteor/app/slashcommands-hide/client/hide.ts b/apps/meteor/app/slashcommands-hide/client/hide.ts
index 99c1eaea7049..c6486053ecc2 100644
--- a/apps/meteor/app/slashcommands-hide/client/hide.ts
+++ b/apps/meteor/app/slashcommands-hide/client/hide.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'hide',
diff --git a/apps/meteor/app/slashcommands-invite/client/client.ts b/apps/meteor/app/slashcommands-invite/client/client.ts
index 729073b785d8..7c8af755d64d 100644
--- a/apps/meteor/app/slashcommands-invite/client/client.ts
+++ b/apps/meteor/app/slashcommands-invite/client/client.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'invite',
diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts
index de525d8c6fc6..06a85301540c 100644
--- a/apps/meteor/app/slashcommands-invite/server/server.ts
+++ b/apps/meteor/app/slashcommands-invite/server/server.ts
@@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor';
import { i18n } from '../../../server/lib/i18n';
import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Invite is a named function that will replace /invite commands
diff --git a/apps/meteor/app/slashcommands-inviteall/client/client.ts b/apps/meteor/app/slashcommands-inviteall/client/client.ts
index f8ab40953d27..5083cd4a83ab 100644
--- a/apps/meteor/app/slashcommands-inviteall/client/client.ts
+++ b/apps/meteor/app/slashcommands-inviteall/client/client.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'invite-all-to',
diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts
index bac4349ec72c..e74bb89899c2 100644
--- a/apps/meteor/app/slashcommands-inviteall/server/server.ts
+++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts
@@ -15,7 +15,7 @@ import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom';
import { createChannelMethod } from '../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
function inviteAll(type: T): SlashCommand['callback'] {
return async function inviteAll({ command, params, message, userId }: SlashCommandCallbackParams): Promise {
diff --git a/apps/meteor/app/slashcommands-join/client/client.ts b/apps/meteor/app/slashcommands-join/client/client.ts
index 417fe1e5cd47..bc8d589f51ac 100644
--- a/apps/meteor/app/slashcommands-join/client/client.ts
+++ b/apps/meteor/app/slashcommands-join/client/client.ts
@@ -1,6 +1,6 @@
import type { Meteor } from 'meteor/meteor';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'join',
diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts
index 33d0278f81a3..6497324ae9e0 100644
--- a/apps/meteor/app/slashcommands-join/server/server.ts
+++ b/apps/meteor/app/slashcommands-join/server/server.ts
@@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor';
import { i18n } from '../../../server/lib/i18n';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'join',
diff --git a/apps/meteor/app/slashcommands-kick/client/client.ts b/apps/meteor/app/slashcommands-kick/client/client.ts
index 475346216f1e..7fc167e17c88 100644
--- a/apps/meteor/app/slashcommands-kick/client/client.ts
+++ b/apps/meteor/app/slashcommands-kick/client/client.ts
@@ -1,6 +1,6 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'kick',
diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts
index 5ca6b45ec835..fdde07b897bf 100644
--- a/apps/meteor/app/slashcommands-kick/server/server.ts
+++ b/apps/meteor/app/slashcommands-kick/server/server.ts
@@ -6,7 +6,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { removeUserFromRoomMethod } from '../../../server/methods/removeUserFromRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'kick',
diff --git a/apps/meteor/app/slashcommands-leave/server/leave.ts b/apps/meteor/app/slashcommands-leave/server/leave.ts
index 42dad0807246..fa108fe18c72 100644
--- a/apps/meteor/app/slashcommands-leave/server/leave.ts
+++ b/apps/meteor/app/slashcommands-leave/server/leave.ts
@@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { leaveRoomMethod } from '../../lib/server/methods/leaveRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Leave is a named function that will replace /leave commands
diff --git a/apps/meteor/app/slashcommands-me/server/me.ts b/apps/meteor/app/slashcommands-me/server/me.ts
index ba6a9f8c82cc..b8b4a593cb73 100644
--- a/apps/meteor/app/slashcommands-me/server/me.ts
+++ b/apps/meteor/app/slashcommands-me/server/me.ts
@@ -1,7 +1,7 @@
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Me is a named function that will replace /me commands
diff --git a/apps/meteor/app/slashcommands-msg/server/server.ts b/apps/meteor/app/slashcommands-msg/server/server.ts
index c6a244b80207..e757938106eb 100644
--- a/apps/meteor/app/slashcommands-msg/server/server.ts
+++ b/apps/meteor/app/slashcommands-msg/server/server.ts
@@ -7,7 +7,7 @@ import { i18n } from '../../../server/lib/i18n';
import { createDirectMessage } from '../../../server/methods/createDirectMessage';
import { executeSendMessage } from '../../lib/server/methods/sendMessage';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Msg is a named function that will replace /msg commands
diff --git a/apps/meteor/app/slashcommands-mute/server/mute.ts b/apps/meteor/app/slashcommands-mute/server/mute.ts
index 03ce960496da..da20ff4fed47 100644
--- a/apps/meteor/app/slashcommands-mute/server/mute.ts
+++ b/apps/meteor/app/slashcommands-mute/server/mute.ts
@@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { muteUserInRoom } from '../../../server/methods/muteUserInRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Mute is a named function that will replace /mute commands
diff --git a/apps/meteor/app/slashcommands-mute/server/unmute.ts b/apps/meteor/app/slashcommands-mute/server/unmute.ts
index 25c0956d49e3..4dc683f4ca93 100644
--- a/apps/meteor/app/slashcommands-mute/server/unmute.ts
+++ b/apps/meteor/app/slashcommands-mute/server/unmute.ts
@@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { unmuteUserInRoom } from '../../../server/methods/unmuteUserInRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
/*
* Unmute is a named function that will replace /unmute commands
diff --git a/apps/meteor/app/slashcommands-open/client/client.ts b/apps/meteor/app/slashcommands-open/client/client.ts
index 987df9599761..99438a24eeb0 100644
--- a/apps/meteor/app/slashcommands-open/client/client.ts
+++ b/apps/meteor/app/slashcommands-open/client/client.ts
@@ -5,7 +5,7 @@ import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator';
import { router } from '../../../client/providers/RouterProvider';
import { Subscriptions, ChatSubscription } from '../../models/client';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'open',
diff --git a/apps/meteor/app/slashcommands-status/client/status.ts b/apps/meteor/app/slashcommands-status/client/status.ts
index 9136ef8f586f..3698b5fda4cb 100644
--- a/apps/meteor/app/slashcommands-status/client/status.ts
+++ b/apps/meteor/app/slashcommands-status/client/status.ts
@@ -2,7 +2,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { dispatchToastMessage } from '../../../client/lib/toast';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'status',
diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts
index 72d92afaf3f2..a2ff6483d398 100644
--- a/apps/meteor/app/slashcommands-status/server/status.ts
+++ b/apps/meteor/app/slashcommands-status/server/status.ts
@@ -5,7 +5,7 @@ import { Users } from '@rocket.chat/models';
import { i18n } from '../../../server/lib/i18n';
import { settings } from '../../settings/server';
import { setUserStatusMethod } from '../../user-status/server/methods/setUserStatus';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'status',
diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts
index f5f5ed58bb0f..f7e47c334b5a 100644
--- a/apps/meteor/app/slashcommands-topic/client/topic.ts
+++ b/apps/meteor/app/slashcommands-topic/client/topic.ts
@@ -5,7 +5,7 @@ import { callbacks } from '../../../lib/callbacks';
import { hasPermission } from '../../authorization/client';
import { ChatRoom } from '../../models/client/models/ChatRoom';
import { sdk } from '../../utils/client/lib/SDKClient';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'topic',
diff --git a/apps/meteor/app/slashcommands-topic/server/topic.ts b/apps/meteor/app/slashcommands-topic/server/topic.ts
index 24fd51d5f509..c1fa6ea283b7 100644
--- a/apps/meteor/app/slashcommands-topic/server/topic.ts
+++ b/apps/meteor/app/slashcommands-topic/server/topic.ts
@@ -2,7 +2,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission';
import { saveRoomSettings } from '../../channel-settings/server/methods/saveRoomSettings';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'topic',
diff --git a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts
index 2fed1e1c7802..7b65fc067031 100644
--- a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts
+++ b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts
@@ -1,4 +1,4 @@
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/client/slashCommand';
slashCommands.add({
command: 'unarchive',
diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts
index d87981bd65a2..4c0c44269d2f 100644
--- a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts
+++ b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts
@@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator';
import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission';
import { unarchiveRoom } from '../../lib/server/functions/unarchiveRoom';
import { settings } from '../../settings/server';
-import { slashCommands } from '../../utils/lib/slashCommand';
+import { slashCommands } from '../../utils/server/slashCommand';
slashCommands.add({
command: 'unarchive',
diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts
index cff2aaefcc5a..e5001b2bff87 100644
--- a/apps/meteor/app/statistics/server/lib/statistics.ts
+++ b/apps/meteor/app/statistics/server/lib/statistics.ts
@@ -542,7 +542,6 @@ export const statistics = {
statistics.totalOTRRooms = await Rooms.findByCreatedOTR().count();
statistics.totalOTR = settings.get('OTR_Count');
statistics.totalBroadcastRooms = await Rooms.findByBroadcast().count();
- statistics.totalRoomsWithActiveLivestream = await Rooms.findByActiveLivestream().count();
statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count');
statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference });
statistics.totalRoomsWithPinned = await Messages.countRoomsWithPinnedMessages({ readPreference });
diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css
index 20b023cc61aa..3120d9c05ff0 100644
--- a/apps/meteor/app/theme/client/imports/general/base_old.css
+++ b/apps/meteor/app/theme/client/imports/general/base_old.css
@@ -776,21 +776,6 @@
padding: 21px 0 10px;
}
- & .start {
- margin-top: 44px;
-
- text-align: center;
-
- & .start__purge-warning {
- margin-top: -33px;
- margin-bottom: 0.5rem;
- padding: 1rem;
-
- border-width: 1px 0 0;
- background: linear-gradient(to bottom, var(--rc-color-alert-message-warning-background) 0%, rgba(255, 255, 255, 0) 100%);
- }
- }
-
& .editing .body {
border-radius: var(--border-radius);
}
diff --git a/apps/meteor/app/threads/server/functions.ts b/apps/meteor/app/threads/server/functions.ts
index 30daef8b8b93..194e482c54ae 100644
--- a/apps/meteor/app/threads/server/functions.ts
+++ b/apps/meteor/app/threads/server/functions.ts
@@ -2,51 +2,57 @@ import type { IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
import { Messages, Subscriptions, ReadReceipts, NotificationQueue } from '@rocket.chat/models';
+import {
+ notifyOnSubscriptionChangedByRoomIdAndUserIds,
+ notifyOnSubscriptionChangedByRoomIdAndUserId,
+} from '../../lib/server/lib/notifyListener';
import { getMentions, getUserIdsFromHighlights } from '../../lib/server/lib/notifyUsersOnMessage';
export async function reply({ tmid }: { tmid?: string }, message: IMessage, parentMessage: IMessage, followers: string[]) {
- const { rid, ts, u } = message;
if (!tmid || isEditedMessage(message)) {
return false;
}
- const { toAll, toHere, mentionIds } = await getMentions(message);
+ const { rid, ts, u } = message;
+
+ const [highlightsUids, threadFollowers, { toAll, toHere, mentionIds }] = await Promise.all([
+ getUserIdsFromHighlights(rid, message),
+ Messages.getThreadFollowsByThreadId(tmid),
+ getMentions(message),
+ ]);
const addToReplies = [
- ...new Set([
- ...followers,
- ...mentionIds,
- ...(Array.isArray(parentMessage.replies) && parentMessage.replies.length ? [u._id] : [parentMessage.u._id, u._id]),
- ]),
+ ...new Set([...followers, ...mentionIds, ...(parentMessage.replies?.length ? [u._id] : [parentMessage.u._id, u._id])]),
];
- const highlightedUserIds = new Set();
- (await getUserIdsFromHighlights(rid, message)).forEach((uid) => highlightedUserIds.add(uid));
- await Messages.updateRepliesByThreadId(tmid, addToReplies, ts);
- await ReadReceipts.setAsThreadById(tmid);
+ const threadFollowersUids = threadFollowers?.filter((userId) => userId !== u._id && !mentionIds.includes(userId)) || [];
- const replies = await Messages.getThreadFollowsByThreadId(tmid);
+ // Notify everyone involved in the thread
+ const notifyOptions = toAll || toHere ? { groupMention: true } : {};
- const repliesFiltered = (replies || []).filter((userId) => userId !== u._id).filter((userId) => !mentionIds.includes(userId));
+ // Notify message mentioned users and highlights
+ const mentionedUsers = [...new Set([...mentionIds, ...highlightsUids])];
- if (toAll || toHere) {
- await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, {
- groupMention: true,
- });
- } else {
- await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, {});
- }
+ const promises = [
+ Messages.updateRepliesByThreadId(tmid, addToReplies, ts),
+ ReadReceipts.setAsThreadById(tmid),
+ Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, threadFollowersUids, tmid, notifyOptions),
+ ];
- const mentionedUsers = new Set([...mentionIds, ...highlightedUserIds]);
- for await (const userId of mentionedUsers) {
- await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, [userId], tmid, { userMention: true });
+ if (mentionedUsers.length) {
+ promises.push(Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, mentionedUsers, tmid, { userMention: true }));
}
- const highlightIds = Array.from(highlightedUserIds);
- if (highlightIds.length) {
- await Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightIds);
- await Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightIds);
+ if (highlightsUids.length) {
+ promises.push(
+ Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightsUids),
+ Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightsUids),
+ );
}
+
+ await Promise.allSettled(promises);
+
+ void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, [...threadFollowersUids, ...mentionedUsers, ...highlightsUids]);
}
export async function follow({ tmid, uid }: { tmid: string; uid: string }) {
@@ -62,20 +68,27 @@ export async function unfollow({ tmid, rid, uid }: { tmid: string; rid: string;
return false;
}
- await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid);
+ const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid);
+ if (removeUnreadThreadResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid);
+ }
await Messages.removeThreadFollowerByThreadId(tmid, uid);
}
export const readThread = async ({ userId, rid, tmid }: { userId: string; rid: string; tmid: string }) => {
- const projection = { tunread: 1 };
- const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection });
+ const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { tunread: 1 } });
if (!sub) {
return;
}
+
// if the thread being marked as read is the last one unread also clear the unread subscription flag
const clearAlert = sub.tunread && sub.tunread?.length <= 1 && sub.tunread.includes(tmid);
- await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert);
+ const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert);
+ if (removeUnreadThreadResponse.modifiedCount) {
+ void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId);
+ }
+
await NotificationQueue.clearQueueByUserId(userId);
};
diff --git a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts
index 179cb5ec12b7..a938dadddb27 100644
--- a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts
+++ b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts
@@ -77,7 +77,7 @@ Meteor.startup(() => {
}
callbacks.add(
'afterSaveMessage',
- async (message, room) => {
+ async (message, { room }) => {
return processThreads(message, room);
},
callbacks.priority.LOW,
diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts
index 1790e0607a62..8ed7093e00d4 100644
--- a/apps/meteor/app/threads/server/methods/followMessage.ts
+++ b/apps/meteor/app/threads/server/methods/followMessage.ts
@@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
import { RateLimiter } from '../../../lib/server';
+import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { follow } from '../functions';
@@ -41,7 +42,13 @@ Meteor.methods({
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
}
- const followResult = await follow({ tmid: message.tmid || message._id, uid });
+ const id = message.tmid || message._id;
+
+ const followResult = await follow({ tmid: id, uid });
+
+ void notifyOnMessageChange({
+ id,
+ });
const isFollowed = true;
await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed);
diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts
index 6371f40af6cb..de4f2683be41 100644
--- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts
+++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts
@@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
import { RateLimiter } from '../../../lib/server';
+import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { unfollow } from '../functions';
@@ -41,7 +42,13 @@ Meteor.methods({
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' });
}
- const unfollowResult = await unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid });
+ const id = message.tmid || message._id;
+
+ const unfollowResult = await unfollow({ rid: message.rid, tmid: id, uid });
+
+ void notifyOnMessageChange({
+ id,
+ });
const isFollowed = false;
await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed);
diff --git a/apps/meteor/app/utils/client/index.ts b/apps/meteor/app/utils/client/index.ts
index fd03ffc3d720..561a1116141b 100644
--- a/apps/meteor/app/utils/client/index.ts
+++ b/apps/meteor/app/utils/client/index.ts
@@ -2,6 +2,6 @@ export { Info } from '../rocketchat.info';
export { getUserPreference } from './lib/getUserPreference';
export { fileUploadIsValidContentType } from './restrictions';
export { getUserAvatarURL } from './getUserAvatarURL';
-export { slashCommands } from '../lib/slashCommand';
+export { slashCommands } from './slashCommand';
export { getURL } from './getURL';
export { APIClient } from './lib/RestApiClient';
diff --git a/apps/meteor/app/utils/lib/slashCommand.ts b/apps/meteor/app/utils/client/slashCommand.ts
similarity index 85%
rename from apps/meteor/app/utils/lib/slashCommand.ts
rename to apps/meteor/app/utils/client/slashCommand.ts
index 47149807bbd8..66e793012fac 100644
--- a/apps/meteor/app/utils/lib/slashCommand.ts
+++ b/apps/meteor/app/utils/client/slashCommand.ts
@@ -6,7 +6,8 @@ import type {
SlashCommandPreviewItem,
SlashCommandPreviews,
} from '@rocket.chat/core-typings';
-import { Meteor } from 'meteor/meteor';
+
+import { InvalidCommandUsage, InvalidPreview } from '../../../client/lib/errors';
interface ISlashCommandAddParams {
command: string;
@@ -69,7 +70,7 @@ export const slashCommands = {
}
if (!message?.rid) {
- throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ throw new InvalidCommandUsage();
}
return cmd.callback({ command, params, message, triggerId, userId });
@@ -85,7 +86,7 @@ export const slashCommands = {
}
if (!message?.rid) {
- throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ throw new InvalidCommandUsage();
}
const previewInfo = await cmd.previewer(command, params, message);
@@ -114,12 +115,12 @@ export const slashCommands = {
}
if (!message?.rid) {
- throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ throw new InvalidCommandUsage();
}
// { id, type, value }
if (!preview.id || !preview.type || !preview.value) {
- throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.');
+ throw new InvalidPreview();
}
return cmd.previewCallback(command, params, message, preview, triggerId);
diff --git a/apps/meteor/app/utils/server/functions/getMongoInfo.ts b/apps/meteor/app/utils/server/functions/getMongoInfo.ts
index 8460e9e4ced0..1caef4a22e32 100644
--- a/apps/meteor/app/utils/server/functions/getMongoInfo.ts
+++ b/apps/meteor/app/utils/server/functions/getMongoInfo.ts
@@ -7,7 +7,6 @@ function getOplogInfo(): { oplogEnabled: boolean; mongo: MongoConnection } {
const oplogEnabled = isWatcherRunning();
- // @ts-expect-error - You're drunk ts
return { oplogEnabled, mongo };
}
diff --git a/apps/meteor/app/utils/server/slashCommand.ts b/apps/meteor/app/utils/server/slashCommand.ts
index dc85fee9b671..27b3c81735f9 100644
--- a/apps/meteor/app/utils/server/slashCommand.ts
+++ b/apps/meteor/app/utils/server/slashCommand.ts
@@ -1,7 +1,139 @@
+import { MeteorError } from '@rocket.chat/core-services';
+import type {
+ IMessage,
+ SlashCommand,
+ SlashCommandOptions,
+ RequiredField,
+ SlashCommandPreviewItem,
+ SlashCommandPreviews,
+} from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
-import { slashCommands } from '../lib/slashCommand';
+interface ISlashCommandAddParams {
+ command: string;
+ callback?: SlashCommand['callback'];
+ options?: SlashCommandOptions;
+ result?: SlashCommand['result'];
+ providesPreview?: boolean;
+ previewer?: SlashCommand['previewer'];
+ previewCallback?: SlashCommand['previewCallback'];
+ appId?: string;
+ description?: string;
+}
+
+export const slashCommands = {
+ commands: {} as Record,
+ add({
+ command,
+ callback,
+ options = {},
+ result,
+ providesPreview = false,
+ previewer,
+ previewCallback,
+ appId,
+ description = '',
+ }: ISlashCommandAddParams): void {
+ if (this.commands[command]) {
+ return;
+ }
+ this.commands[command] = {
+ command,
+ callback,
+ params: options.params,
+ description: options.description || description,
+ permission: options.permission,
+ clientOnly: options.clientOnly || false,
+ result,
+ providesPreview: Boolean(providesPreview),
+ previewer,
+ previewCallback,
+ appId,
+ } as SlashCommand;
+ },
+ async run({
+ command,
+ message,
+ params,
+ triggerId,
+ userId,
+ }: {
+ command: string;
+ params: string;
+ message: RequiredField, 'rid' | '_id'>;
+ userId: string;
+ triggerId?: string | undefined;
+ }): Promise {
+ const cmd = this.commands[command];
+ if (typeof cmd?.callback !== 'function') {
+ return;
+ }
+
+ if (!message?.rid) {
+ throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ }
+
+ return cmd.callback({ command, params, message, triggerId, userId });
+ },
+ async getPreviews(
+ command: string,
+ params: string,
+ message: RequiredField, 'rid'>,
+ ): Promise {
+ const cmd = this.commands[command];
+ if (typeof cmd?.previewer !== 'function') {
+ return;
+ }
+
+ if (!message?.rid) {
+ throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ }
+
+ const previewInfo = await cmd.previewer(command, params, message);
+
+ if (!previewInfo?.items?.length) {
+ return;
+ }
+
+ // A limit of ten results, to save time and bandwidth
+ if (previewInfo.items.length >= 10) {
+ previewInfo.items = previewInfo.items.slice(0, 10);
+ }
+
+ return previewInfo;
+ },
+ async executePreview(
+ command: string,
+ params: string,
+ message: Pick & Partial>,
+ preview: SlashCommandPreviewItem,
+ triggerId?: string,
+ ) {
+ const cmd = this.commands[command];
+ if (typeof cmd?.previewCallback !== 'function') {
+ return;
+ }
+
+ if (!message?.rid) {
+ throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
+ }
+
+ // { id, type, value }
+ if (!preview.id || !preview.type || !preview.value) {
+ throw new MeteorError('error-invalid-preview', 'Preview Item must have an id, type, and value.');
+ }
+
+ return cmd.previewCallback(command, params, message, preview, triggerId);
+ },
+};
+
+declare module '@rocket.chat/ddp-client' {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ interface ServerMethods {
+ slashCommand(params: { cmd: string; params: string; msg: IMessage; triggerId: string }): unknown;
+ }
+}
Meteor.methods({
async slashCommand(command) {
@@ -27,5 +159,3 @@ Meteor.methods({
});
},
});
-
-export { slashCommands };
diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx
index 11eddf934055..94fdfe25a92d 100644
--- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx
+++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx
@@ -1,10 +1,11 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
-import { renderHook } from '@testing-library/react-hooks';
+import { renderHook, waitFor } from '@testing-library/react';
import { useAuditMenu } from './useAuditMenu';
it('should return an empty array of items if doesn`t have license', async () => {
- const { result, waitFor } = renderHook(() => useAuditMenu(), {
+ const { result } = renderHook(() => useAuditMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
// @ts-expect-error: just for testing
@@ -18,13 +19,12 @@ it('should return an empty array of items if doesn`t have license', async () =>
.build(),
});
- await waitFor(() => result.all.length > 1);
-
- expect(result.current).toEqual([]);
+ await waitFor(() => expect(result.current).toEqual([]));
});
it('should return an empty array of items if have license and not have permissions', async () => {
- const { result, waitFor } = renderHook(() => useAuditMenu(), {
+ const { result } = renderHook(() => useAuditMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
@@ -41,13 +41,12 @@ it('should return an empty array of items if have license and not have permissio
.build(),
});
- await waitFor(() => result.all.length > 1);
-
- expect(result.current).toEqual([]);
+ await waitFor(() => expect(result.current).toEqual([]));
});
it('should return auditItems if have license and permissions', async () => {
- const { result, waitFor } = renderHook(() => useAuditMenu(), {
+ const { result } = renderHook(() => useAuditMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
@@ -65,12 +64,12 @@ it('should return auditItems if have license and permissions', async () => {
.build(),
});
- await waitFor(() => result.current.length > 0);
-
- expect(result.current[0].items[0]).toEqual(
- expect.objectContaining({
- id: 'messages',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[0]).toEqual(
+ expect.objectContaining({
+ id: 'messages',
+ }),
+ ),
);
expect(result.current[0].items[1]).toEqual(
@@ -81,7 +80,8 @@ it('should return auditItems if have license and permissions', async () => {
});
it('should return auditMessages item if have license and can-audit permission', async () => {
- const { result, waitFor } = renderHook(() => useAuditMenu(), {
+ const { result } = renderHook(() => useAuditMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
@@ -98,17 +98,18 @@ it('should return auditMessages item if have license and can-audit permission',
.build(),
});
- await waitFor(() => result.current.length > 0);
-
- expect(result.current[0].items[0]).toEqual(
- expect.objectContaining({
- id: 'messages',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[0]).toEqual(
+ expect.objectContaining({
+ id: 'messages',
+ }),
+ ),
);
});
it('should return audiLogs item if have license and can-audit-log permission', async () => {
- const { result, waitFor } = renderHook(() => useAuditMenu(), {
+ const { result } = renderHook(() => useAuditMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
@@ -125,11 +126,11 @@ it('should return audiLogs item if have license and can-audit-log permission', a
.build(),
});
- await waitFor(() => result.current.length > 0);
-
- expect(result.current[0].items[0]).toEqual(
- expect.objectContaining({
- id: 'auditLog',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[0]).toEqual(
+ expect.objectContaining({
+ id: 'auditLog',
+ }),
+ ),
);
});
diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx
index 2a3d277e69fe..d2d1e36ca05e 100644
--- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx
+++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx
@@ -1,11 +1,12 @@
import { UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui';
import { mockAppRoot } from '@rocket.chat/mock-providers';
-import { renderHook } from '@testing-library/react-hooks';
+import { renderHook, waitFor } from '@testing-library/react';
import { useMarketPlaceMenu } from './useMarketPlaceMenu';
it('should return and empty array if the user does not have `manage-apps` and `access-marketplace` permission', () => {
const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [])
.build(),
@@ -16,6 +17,7 @@ it('should return and empty array if the user does not have `manage-apps` and `a
it('should return `explore` and `installed` items if the user has `access-marketplace` permission', () => {
const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [])
.withPermission('access-marketplace')
@@ -37,6 +39,7 @@ it('should return `explore` and `installed` items if the user has `access-market
it('should return `explore`, `installed` and `requested` items if the user has `manage-apps` permission', () => {
const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [])
.withEndpoint('GET', '/apps/app-request/stats', () => ({
@@ -69,7 +72,8 @@ it('should return `explore`, `installed` and `requested` items if the user has `
});
it('should return one action from the server with no conditions', async () => {
- const { result, waitForValueToChange } = renderHook(() => useMarketPlaceMenu(), {
+ const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [
{
@@ -101,18 +105,19 @@ it('should return one action from the server with no conditions', async () => {
}),
);
- await waitForValueToChange(() => result.current[0].items[3]);
-
- expect(result.current[0].items[3]).toEqual(
- expect.objectContaining({
- id: 'APP_ID_ACTION_ID',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[3]).toEqual(
+ expect.objectContaining({
+ id: 'APP_ID_ACTION_ID',
+ }),
+ ),
);
});
describe('Marketplace menu with role conditions', () => {
it('should return the action if the user has admin role', async () => {
- const { result, waitForValueToChange } = renderHook(() => useMarketPlaceMenu(), {
+ const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [
{
@@ -149,17 +154,18 @@ describe('Marketplace menu with role conditions', () => {
}),
);
- await waitForValueToChange(() => result.current[0].items[3]);
-
- expect(result.current[0].items[3]).toEqual(
- expect.objectContaining({
- id: 'APP_ID_ACTION_ID',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[3]).toEqual(
+ expect.objectContaining({
+ id: 'APP_ID_ACTION_ID',
+ }),
+ ),
);
});
it('should return filter the action if the user doesn`t have admin role', async () => {
const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [
{
@@ -206,7 +212,8 @@ describe('Marketplace menu with role conditions', () => {
describe('Marketplace menu with permission conditions', () => {
it('should return the action if the user has manage-apps permission', async () => {
- const { result, waitForValueToChange } = renderHook(() => useMarketPlaceMenu(), {
+ const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [
{
@@ -241,17 +248,18 @@ describe('Marketplace menu with permission conditions', () => {
}),
);
- await waitForValueToChange(() => result.current[0].items[3]);
-
- expect(result.current[0].items[3]).toEqual(
- expect.objectContaining({
- id: 'APP_ID_ACTION_ID',
- }),
+ await waitFor(() =>
+ expect(result.current[0].items[3]).toEqual(
+ expect.objectContaining({
+ id: 'APP_ID_ACTION_ID',
+ }),
+ ),
);
});
it('should return filter the action if the user doesn`t have `any` permission', async () => {
const { result } = renderHook(() => useMarketPlaceMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/apps/actionButtons', () => [
{
diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx
index 1315d1053392..ba100fe79783 100644
--- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx
+++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx
@@ -1,10 +1,11 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
-import { renderHook } from '@testing-library/react-hooks';
+import { renderHook, waitFor } from '@testing-library/react';
import { useAdministrationMenu } from './useAdministrationMenu';
it('should return omnichannel item if has `view-livechat-manager` permission ', async () => {
- const { result, waitFor } = renderHook(() => useAdministrationMenu(), {
+ const { result } = renderHook(() => useAdministrationMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
// @ts-expect-error this is a mock
@@ -19,17 +20,18 @@ it('should return omnichannel item if has `view-livechat-manager` permission ',
.build(),
});
- await waitFor(() => !!result.current.length);
-
- expect(result.current[0].items[0]).toEqual(
- expect.objectContaining({
- id: 'omnichannel',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[0]).toEqual(
+ expect.objectContaining({
+ id: 'omnichannel',
+ }),
+ ),
);
});
it('should show administration item if has at least one admin permission', async () => {
- const { result, waitFor } = renderHook(() => useAdministrationMenu(), {
+ const { result } = renderHook(() => useAdministrationMenu(), {
+ legacyRoot: true,
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
// @ts-expect-error this is a mock
@@ -44,11 +46,11 @@ it('should show administration item if has at least one admin permission', async
.build(),
});
- await waitFor(() => !!result.current.length);
-
- expect(result.current[0].items[0]).toEqual(
- expect.objectContaining({
- id: 'workspace',
- }),
+ await waitFor(() =>
+ expect(result.current[0]?.items[0]).toEqual(
+ expect.objectContaining({
+ id: 'workspace',
+ }),
+ ),
);
});
diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx
index 795182df8465..ebd92f0095e3 100644
--- a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx
+++ b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx
@@ -8,10 +8,10 @@ type ContextualbarHeaderProps = {
children: ReactNode;
} & ComponentPropsWithoutRef;
-const ContextualbarHeader = (props: ContextualbarHeaderProps) => (
+const ContextualbarHeader = ({ expanded, ...props }: ContextualbarHeaderProps) => (
-
+
diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx
index 99e62bac1a60..530bd1404dc7 100644
--- a/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx
+++ b/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx
@@ -1,4 +1,3 @@
-import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -32,28 +31,28 @@ const sections = [regular, danger];
describe('Room Actions Menu', () => {
it('should render kebab menu with the list content', async () => {
- render();
+ render(, { legacyRoot: true });
- userEvent.click(screen.getByRole('button'));
+ await userEvent.click(screen.getByRole('button'));
expect(await screen.findByText('Edit')).toBeInTheDocument();
expect(await screen.findByText('Delete')).toBeInTheDocument();
});
it('should have two different sections, regular and danger', async () => {
- render();
+ render(, { legacyRoot: true });
- userEvent.click(screen.getByRole('button'));
+ await userEvent.click(screen.getByRole('button'));
expect(screen.getAllByRole('presentation')).toHaveLength(2);
expect(screen.getByRole('separator')).toBeInTheDocument();
});
it('should call the action when item clicked', async () => {
- render();
+ render(, { legacyRoot: true });
- userEvent.click(screen.getByRole('button'));
- userEvent.click(screen.getAllByRole('menuitem')[0]);
+ await userEvent.click(screen.getByRole('button'));
+ await userEvent.click(screen.getAllByRole('menuitem')[0]);
expect(mockedFunction).toHaveBeenCalled();
});
diff --git a/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx
index 0ef7235729c4..b47b6abf7b00 100644
--- a/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx
+++ b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx
@@ -1,6 +1,5 @@
import { useSetModal } from '@rocket.chat/ui-contexts';
-import { act, screen } from '@testing-library/react';
-import { renderHook } from '@testing-library/react-hooks';
+import { act, screen, renderHook } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ReactElement } from 'react';
import React, { Suspense } from 'react';
@@ -8,12 +7,11 @@ import React, { Suspense } from 'react';
import ModalProviderWithRegion from '../../providers/ModalProvider/ModalProviderWithRegion';
import GenericModal from './GenericModal';
-import '@testing-library/jest-dom';
-
const renderModal = (modalElement: ReactElement) => {
const {
result: { current: setModal },
} = renderHook(() => useSetModal(), {
+ legacyRoot: true,
wrapper: ({ children }) => (
{children}
@@ -34,11 +32,11 @@ describe('callbacks', () => {
renderModal();
- expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument();
+ expect(await screen.findByRole('heading', { name: 'Modal' })).toBeInTheDocument();
- userEvent.keyboard('{Escape}');
+ await userEvent.keyboard('{Escape}');
- expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument();
+ expect(screen.queryByRole('heading', { name: 'Modal' })).not.toBeInTheDocument();
expect(handleClose).toHaveBeenCalled();
});
@@ -49,9 +47,9 @@ describe('callbacks', () => {
const { setModal } = renderModal();
- expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument();
+ expect(await screen.findByRole('heading', { name: 'Modal' })).toBeInTheDocument();
- userEvent.click(screen.getByRole('button', { name: 'Ok', exact: true }));
+ await userEvent.click(screen.getByRole('button', { name: 'Ok' }));
expect(handleConfirm).toHaveBeenCalled();
@@ -59,7 +57,7 @@ describe('callbacks', () => {
setModal(null);
});
- expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument();
+ expect(screen.queryByRole('heading', { name: 'Modal' })).not.toBeInTheDocument();
expect(handleClose).not.toHaveBeenCalled();
});
@@ -70,9 +68,9 @@ describe('callbacks', () => {
const { setModal } = renderModal();
- expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument();
+ expect(await screen.findByRole('heading', { name: 'Modal' })).toBeInTheDocument();
- userEvent.click(screen.getByRole('button', { name: 'Cancel', exact: true }));
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(handleCancel).toHaveBeenCalled();
@@ -80,7 +78,7 @@ describe('callbacks', () => {
setModal(null);
});
- expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument();
+ expect(screen.queryByRole('heading', { name: 'Modal' })).not.toBeInTheDocument();
expect(handleClose).not.toHaveBeenCalled();
});
diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx
index fb97b0132f85..8db42e8c649d 100644
--- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx
+++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx
@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
-import '@testing-library/jest-dom/extend-expect';
import { createRenteionPolicySettingsMock as createMock } from '../../../tests/mocks/client/mockRetentionPolicySettings';
import { createFakeRoom } from '../../../tests/mocks/data';
@@ -15,13 +14,17 @@ beforeEach(() => {
describe('RetentionPolicyCallout', () => {
it('Should render callout if settings are valid', () => {
const fakeRoom = createFakeRoom({ t: 'c' });
- render(, { wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000 }) });
+ render(, {
+ legacyRoot: true,
+ wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000 }),
+ });
expect(screen.getByRole('alert')).toHaveTextContent('a minute June 1, 2024, 12:30 AM');
});
it('Should not render callout if settings are invalid', () => {
const fakeRoom = createFakeRoom({ t: 'c' });
render(, {
+ legacyRoot: true,
wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000, advancedPrecisionCron: '* * * 12 *', advancedPrecision: true }),
});
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
diff --git a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.spec.tsx b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.spec.tsx
new file mode 100644
index 000000000000..87f7f70fbfbb
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.spec.tsx
@@ -0,0 +1,48 @@
+import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import TranscriptModal from './TranscriptModal';
+
+const room = {
+ open: true,
+ v: { token: '1234567890' },
+ transcriptRequest: {
+ email: 'example@example.com',
+ subject: 'Transcript of livechat conversation',
+ },
+} as IOmnichannelRoom;
+
+const defaultProps = {
+ room,
+ email: 'test@example.com',
+ onRequest: () => null,
+ onSend: () => null,
+ onCancel: () => null,
+ onDiscard: () => null,
+};
+
+it('should show Undo request button when roomOpen is true and transcriptRequest exist', async () => {
+ const onDiscardMock = jest.fn();
+ render(, { legacyRoot: true });
+
+ const undoRequestButton = await screen.findByText('Undo_request');
+ await userEvent.click(undoRequestButton);
+
+ expect(onDiscardMock).toHaveBeenCalled();
+});
+
+it('should show Request button when roomOpen is true and transcriptRequest not exist', async () => {
+ render(, { legacyRoot: true });
+
+ const requestBtn = await screen.findByRole('button', { name: 'request-button' });
+ expect(requestBtn).toBeInTheDocument();
+});
+
+it('should show Send button when roomOpen is false', async () => {
+ render(, { legacyRoot: true });
+
+ const sendBtn = await screen.findByRole('button', { name: 'send-button' });
+ expect(sendBtn).toBeInTheDocument();
+});
diff --git a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx
index 95bda1e89107..c06b6a190465 100644
--- a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx
+++ b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx
@@ -21,8 +21,7 @@ const TranscriptModal = ({ email: emailDefault = '', room, onRequest, onSend, on
handleSubmit,
setValue,
setFocus,
- watch,
- formState: { errors, isValid, isSubmitting },
+ formState: { errors, isSubmitting },
} = useForm({
defaultValues: { email: emailDefault || '', subject: t('Transcript_of_your_livechat_conversation') },
});
@@ -56,7 +55,7 @@ const TranscriptModal = ({ email: emailDefault = '', room, onRequest, onSend, on
}
}, [setValue, transcriptRequest]);
- const canSubmit = isValid && Boolean(watch('subject'));
+ // const canSubmit = isValid && Boolean(watch('subject'));
return (
} {...props}>
@@ -103,12 +102,12 @@ const TranscriptModal = ({ email: emailDefault = '', room, onRequest, onSend, on
)}
{roomOpen && !transcriptRequest && (
-