diff --git a/.changeset/afraid-guests-jog.md b/.changeset/afraid-guests-jog.md
new file mode 100644
index 000000000000..420b9bb5d329
--- /dev/null
+++ b/.changeset/afraid-guests-jog.md
@@ -0,0 +1,6 @@
+---
+"@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/bump-patch-1722087664914.md b/.changeset/bump-patch-1722087664914.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1722087664914.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1722559871139.md b/.changeset/bump-patch-1722559871139.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1722559871139.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1722695753777.md b/.changeset/bump-patch-1722695753777.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1722695753777.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1722930641296.md b/.changeset/bump-patch-1722930641296.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1722930641296.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1723039032546.md b/.changeset/bump-patch-1723039032546.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1723039032546.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/bump-patch-1723151441289.md b/.changeset/bump-patch-1723151441289.md
new file mode 100644
index 000000000000..e1eaa7980afb
--- /dev/null
+++ b/.changeset/bump-patch-1723151441289.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Bump @rocket.chat/meteor version.
diff --git a/.changeset/chatty-hounds-hammer.md b/.changeset/chatty-hounds-hammer.md
new file mode 100644
index 000000000000..1a2d3a7de559
--- /dev/null
+++ b/.changeset/chatty-hounds-hammer.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..670fa24887b7
--- /dev/null
+++ b/.changeset/chilled-yaks-beg.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..a7724b126695
--- /dev/null
+++ b/.changeset/chilly-papayas-march.md
@@ -0,0 +1,5 @@
+---
+"@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/cuddly-brooms-approve.md b/.changeset/cuddly-brooms-approve.md
new file mode 100644
index 000000000000..24905bb91c62
--- /dev/null
+++ b/.changeset/cuddly-brooms-approve.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..b66ca5157cd5
--- /dev/null
+++ b/.changeset/dry-pumas-draw.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..b4bd075ef654
--- /dev/null
+++ b/.changeset/empty-readers-teach.md
@@ -0,0 +1,8 @@
+---
+"@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/fast-buttons-shake.md b/.changeset/fast-buttons-shake.md
new file mode 100644
index 000000000000..6281fc9941ec
--- /dev/null
+++ b/.changeset/fast-buttons-shake.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+Fixed an issue where FCM actions did not respect environment's proxy settings
diff --git a/.changeset/funny-snails-promise.md b/.changeset/funny-snails-promise.md
new file mode 100644
index 000000000000..bdd74a60b1e9
--- /dev/null
+++ b/.changeset/funny-snails-promise.md
@@ -0,0 +1,10 @@
+---
+"@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
new file mode 100644
index 000000000000..e2364ccb05e5
--- /dev/null
+++ b/.changeset/funny-wolves-tie.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixed issue where bad word filtering was not working in the UI for messages
diff --git a/.changeset/grumpy-worms-appear.md b/.changeset/grumpy-worms-appear.md
new file mode 100644
index 000000000000..fb9fab77b24c
--- /dev/null
+++ b/.changeset/grumpy-worms-appear.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..2dfb2151ced0
--- /dev/null
+++ b/.changeset/happy-peaches-nail.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..f1d7bb6f3f0e
--- /dev/null
+++ b/.changeset/hip-queens-taste.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..4e50b172e17e
--- /dev/null
+++ b/.changeset/hungry-wombats-act.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed an issue where non-encrypted attachments were not being downloaded
diff --git a/.changeset/large-vans-attack.md b/.changeset/large-vans-attack.md
new file mode 100644
index 000000000000..c1008b2ca06f
--- /dev/null
+++ b/.changeset/large-vans-attack.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..3e23797025e1
--- /dev/null
+++ b/.changeset/lucky-beds-glow.md
@@ -0,0 +1,7 @@
+---
+'@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
new file mode 100644
index 000000000000..79deda53edfc
--- /dev/null
+++ b/.changeset/lucky-countries-look.md
@@ -0,0 +1,5 @@
+---
+'@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
new file mode 100644
index 000000000000..8f37283c6a96
--- /dev/null
+++ b/.changeset/many-tables-love.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..c92293d6ae95
--- /dev/null
+++ b/.changeset/mean-hairs-move.md
@@ -0,0 +1,5 @@
+---
+'@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/nervous-rockets-impress.md b/.changeset/nervous-rockets-impress.md
new file mode 100644
index 000000000000..26e9276193de
--- /dev/null
+++ b/.changeset/nervous-rockets-impress.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..7d4e7cd3a57e
--- /dev/null
+++ b/.changeset/new-balloons-speak.md
@@ -0,0 +1,5 @@
+---
+'@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-scissors-love.md b/.changeset/new-scissors-love.md
new file mode 100644
index 000000000000..fb962407b353
--- /dev/null
+++ b/.changeset/new-scissors-love.md
@@ -0,0 +1,12 @@
+---
+'@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
new file mode 100644
index 000000000000..e99e4f219ef9
--- /dev/null
+++ b/.changeset/nice-laws-eat.md
@@ -0,0 +1,15 @@
+---
+'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/perfect-coins-camp.md b/.changeset/perfect-coins-camp.md
new file mode 100644
index 000000000000..4dbddf965742
--- /dev/null
+++ b/.changeset/perfect-coins-camp.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..2f524c7e5f10
--- /dev/null
+++ b/.changeset/polite-foxes-repair.md
@@ -0,0 +1,5 @@
+---
+'@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
new file mode 100644
index 000000000000..f38ef1f92367
--- /dev/null
+++ b/.changeset/popular-trees-lay.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Removed 'Hide' option in the room menu for Omnichannel conversations.
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 000000000000..40c93f4a63bd
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,118 @@
+{
+ "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",
+ "bump-patch-1722695753777",
+ "bump-patch-1722930641296",
+ "bump-patch-1723039032546",
+ "bump-patch-1723151441289",
+ "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
new file mode 100644
index 000000000000..556fa3af80e1
--- /dev/null
+++ b/.changeset/proud-waves-bathe.md
@@ -0,0 +1,6 @@
+---
+"@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/quick-ducks-live.md b/.changeset/quick-ducks-live.md
new file mode 100644
index 000000000000..ad628c13d087
--- /dev/null
+++ b/.changeset/quick-ducks-live.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..187bd9d09ddc
--- /dev/null
+++ b/.changeset/rare-penguins-hope.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..61cb0d2b7586
--- /dev/null
+++ b/.changeset/red-numbers-happen.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..ddf76535087e
--- /dev/null
+++ b/.changeset/red-vans-shave.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..16741e31e54a
--- /dev/null
+++ b/.changeset/rich-carpets-brush.md
@@ -0,0 +1,5 @@
+---
+'@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/rotten-eggs-end.md b/.changeset/rotten-eggs-end.md
new file mode 100644
index 000000000000..7d0ad6ee5047
--- /dev/null
+++ b/.changeset/rotten-eggs-end.md
@@ -0,0 +1,7 @@
+---
+"@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/selfish-emus-sing.md b/.changeset/selfish-emus-sing.md
new file mode 100644
index 000000000000..315d674a1857
--- /dev/null
+++ b/.changeset/selfish-emus-sing.md
@@ -0,0 +1,6 @@
+---
+"@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
new file mode 100644
index 000000000000..40ee9f8fbb55
--- /dev/null
+++ b/.changeset/shaggy-hats-raise.md
@@ -0,0 +1,5 @@
+---
+"@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/sixty-nails-clean.md b/.changeset/sixty-nails-clean.md
new file mode 100644
index 000000000000..7d13e02f0bd3
--- /dev/null
+++ b/.changeset/sixty-nails-clean.md
@@ -0,0 +1,5 @@
+---
+'@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/smooth-lobsters-flash.md b/.changeset/smooth-lobsters-flash.md
new file mode 100644
index 000000000000..541d5069ee9c
--- /dev/null
+++ b/.changeset/smooth-lobsters-flash.md
@@ -0,0 +1,5 @@
+---
+'@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
new file mode 100644
index 000000000000..7273ddcffca4
--- /dev/null
+++ b/.changeset/soft-donkeys-thank.md
@@ -0,0 +1,8 @@
+---
+"@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
new file mode 100644
index 000000000000..2d1076845fa9
--- /dev/null
+++ b/.changeset/sour-forks-breathe.md
@@ -0,0 +1,5 @@
+---
+"@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/thin-windows-reply.md b/.changeset/thin-windows-reply.md
new file mode 100644
index 000000000000..1a32e1ddebfb
--- /dev/null
+++ b/.changeset/thin-windows-reply.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixes an issue not displaying all groups in settings list
diff --git a/.changeset/violet-brooms-press.md b/.changeset/violet-brooms-press.md
new file mode 100644
index 000000000000..632026d6fe2e
--- /dev/null
+++ b/.changeset/violet-brooms-press.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)
diff --git a/.changeset/weak-insects-sort.md b/.changeset/weak-insects-sort.md
new file mode 100644
index 000000000000..cbbe7c4aa08c
--- /dev/null
+++ b/.changeset/weak-insects-sort.md
@@ -0,0 +1,5 @@
+---
+"@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
new file mode 100644
index 000000000000..abaa9c683d65
--- /dev/null
+++ b/.changeset/weak-pets-talk.md
@@ -0,0 +1,7 @@
+---
+'@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
new file mode 100644
index 000000000000..a2d435495cd7
--- /dev/null
+++ b/.changeset/weak-taxis-design.md
@@ -0,0 +1,5 @@
+---
+'@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
new file mode 100644
index 000000000000..91748a43c677
--- /dev/null
+++ b/.changeset/weak-tigers-suffer.md
@@ -0,0 +1,7 @@
+---
+"@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
new file mode 100644
index 000000000000..42c9409d9ef3
--- /dev/null
+++ b/.changeset/witty-bats-develop.md
@@ -0,0 +1,13 @@
+---
+"@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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index c2a05cc61166..5a077e74a1d8 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -35,6 +35,8 @@ If you are experiencing a bug please search our issues to be sure it is not alre
### Server Setup Information:
- Version of Rocket.Chat Server:
+- License Type:
+- Number of Users:
- Operating System:
- Deployment Method:
- Number of Running Instances:
diff --git a/.github/actions/update-version-durability/action.yml b/.github/actions/update-version-durability/action.yml
new file mode 100644
index 000000000000..803158ae1a25
--- /dev/null
+++ b/.github/actions/update-version-durability/action.yml
@@ -0,0 +1,24 @@
+name: Update Version Durability
+description: Update Version Durability page on Document360
+
+inputs:
+ GH_TOKEN:
+ required: true
+ description: GitHub API Token
+ type: string
+ D360_TOKEN:
+ required: true
+ description: Document360 API Token
+ type: string
+ D360_ARTICLE_ID:
+ required: true
+ description: Document360 Article ID
+ type: string
+ PUBLISH:
+ required: true
+ description: Publish Draft
+ type: boolean
+
+runs:
+ using: node20
+ main: index.js
diff --git a/.github/actions/update-version-durability/index.js b/.github/actions/update-version-durability/index.js
new file mode 100644
index 000000000000..d96fd7ee4554
--- /dev/null
+++ b/.github/actions/update-version-durability/index.js
@@ -0,0 +1,217 @@
+import 'colors';
+import axios from 'axios';
+import * as Diff from 'diff';
+import semver from 'semver';
+import crypto from 'crypto';
+import fs from 'fs/promises';
+import BeautyHtml from 'beauty-html';
+import { DOMParser } from 'xmldom';
+import core from '@actions/core';
+import { Octokit } from '@octokit/rest';
+
+const D360_TOKEN = core.getInput('D360_TOKEN');
+const D360_ARTICLE_ID = core.getInput('D360_ARTICLE_ID');
+const PUBLISH = core.getInput('PUBLISH') === 'true';
+
+const octokit = new Octokit({
+ auth: core.getInput('GH_TOKEN'),
+});
+
+
+async function requestDocument360(method = 'get', api, data = {}) {
+ return axios.request({
+ method,
+ maxBodyLength: Infinity,
+ url: `https://apihub.us.document360.io/v1/${api}`,
+ headers: {
+ 'accept': 'application/json',
+ 'api_token': D360_TOKEN,
+ },
+ data,
+ });
+}
+
+function md5(text) {
+ return crypto.createHash('md5').update(text).digest("hex");
+}
+
+async function generateTable({ owner, repo } = {}) {
+ const response = await requestDocument360('get', `Articles/${D360_ARTICLE_ID}/en`);
+
+ // console.log(response.data.data);
+
+ // const releasesResult = JSON.parse(await fs.readFile('/tmp/releasesResult'));
+ const releasesResult = await octokit.paginate(octokit.repos.listReleases.endpoint.merge({ owner, repo, per_page: 100 }));
+ // await fs.writeFile('/tmp/releasesResult', JSON.stringify(releasesResult));
+
+ const releases = releasesResult
+ .filter((release) => !release.tag_name.includes('-rc') && semver.gte(release.tag_name, '1.0.0'))
+ .sort((a, b) => semver.compare(b.tag_name, a.tag_name));
+
+ const releasesMap = {};
+
+ for (const release of releases) {
+ release.releaseDate = new Date(release.published_at);
+
+ releasesMap[release.tag_name] = release;
+ }
+
+ let index = 0;
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const release = releases[index];
+
+ release.minor_tag = release.tag_name.replace(/\.\d+$/, '');
+ release.minorRelease = releasesMap[`${release.minor_tag}.0`];
+
+ if (!releases[index + 1]) {
+ break;
+ }
+
+ const currentVersion = semver.parse(release.tag_name);
+ const previousVersion = semver.parse(releases[index + 1].tag_name);
+
+ releases[index + 1].nextRelease = release;
+
+ // Remove duplicated due to patches
+ if (currentVersion.major === previousVersion.major && currentVersion.minor === previousVersion.minor) {
+ releases.splice(index + 1, 1);
+ continue;
+ }
+
+ index++;
+ }
+
+ releases[0].last = true;
+
+ const releaseData = [];
+
+ for (const { tag_name, html_url, lts, last, nextRelease, minorRelease, minor_tag} of releases) {
+ let supportDate;
+ let supportDateStart;
+
+ let releasedAt = new Date(minorRelease.releaseDate);
+ releasedAt.setDate(1);
+
+ let minorDate = new Date(minorRelease.releaseDate);
+ minorDate.setDate(1);
+ supportDateStart = minorDate;
+ supportDate = new Date(minorDate);
+ supportDate.setMonth(supportDate.getMonth() + (lts ? 6 : 6));
+
+ releaseData.push({
+ release: {
+ version: minor_tag,
+ releasedAt,
+ extendedSupport: {
+ start: supportDateStart,
+ end: supportDate,
+ },
+ lts: lts === true,
+ },
+ latestPatch: {
+ version: tag_name,
+ url: html_url,
+ }
+ })
+ }
+
+ function header({data, salt = ''}) {
+ return [
+ '
',
+ ` ${data} `,
+ ' | ',
+ ].join('');
+ }
+
+ function line({data, salt = ''}) {
+ return [
+ '',
+ ` ${data} `,
+ ' | ',
+ ].join('');
+ }
+
+ const text = [
+ '',
+ header({data: 'Rocket.Chat Release'}),
+ header({data: 'Released At'}),
+ header({data: 'End of Life'}),
+ '
',
+ ];
+
+ releaseData.forEach(({release, latestPatch}) => {
+ const releasedAt = release.releasedAt.toLocaleString('en', { month: 'short', year: "numeric" });
+ const endOfLife = !release.extendedSupport
+ ? 'TBD'
+ : release.extendedSupport.end.toLocaleString('en', { month: 'short', year: "numeric" });
+ const link = `${release.version} (${latestPatch.version})`;
+
+ text.push(
+ '',
+ line({data: link}),
+ line({data: releasedAt, salt: release.version}),
+ line({data: endOfLife, salt: release.version}),
+ '
',
+ );
+ });
+
+ const content = response.data.data.html_content.replace(/.+(\n.+)*<\/tbody>/m, `${text.join('').replace(/\t|\n/g, '')}`)
+
+ // console.log(content);
+
+ const parser = new BeautyHtml({ parser: DOMParser });
+ const diff = Diff.diffLines(parser.beautify(response.data.data.html_content), parser.beautify(content), { ignoreWhitespace: true, newlineIsToken: false });
+ diff.forEach((item) => {
+ let color = 'green';
+
+ if (item.removed) {
+ color = 'red';
+ }
+
+ if (item.removed || item.added) {
+ item.value.split('\n').forEach((line) => {
+ if (line === '') { return };
+ console.log(`${item.removed ? '-' : '+'} ${line}`[color]);
+ })
+ }
+ });
+
+ if (diff.length === 1) {
+ console.log('No changes found');
+ return;
+ }
+
+ if (response.data.data.status === 3) {
+ console.log('forking article', response.data.data.version_number);
+
+ const forkResponse = await requestDocument360('put', `Articles/${D360_ARTICLE_ID}/fork`, {
+ lang_code: "en",
+ user_id: "2511fd00-9558-4826-8d8c-4cc0c110f89c",
+ version_number: response.data.data.version_number,
+ });
+
+ console.log(forkResponse.data);
+ }
+
+ console.log('Updating article');
+ const updateResponse = await requestDocument360('put', `Articles/${D360_ARTICLE_ID}/en`, {
+ content,
+ });
+
+ console.log(updateResponse.data);
+
+ if (PUBLISH) {
+ console.log('publishing article', updateResponse.data.data.version_number);
+
+ const forkResponse = await requestDocument360('post', `Articles/${D360_ARTICLE_ID}/en/publish`, {
+ user_id: "2511fd00-9558-4826-8d8c-4cc0c110f89c",
+ version_number: updateResponse.data.data.version_number,
+ publish_message: 'Update support versions table via GitHub Action',
+ });
+
+ console.log(forkResponse.data);
+ }
+}
+
+generateTable({ owner: 'RocketChat', repo: 'Rocket.Chat' });
diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json
new file mode 100644
index 000000000000..889d959cba6d
--- /dev/null
+++ b/.github/actions/update-version-durability/package-lock.json
@@ -0,0 +1,378 @@
+{
+ "name": "scripts",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@actions/core": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz",
+ "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==",
+ "requires": {
+ "@actions/http-client": "^2.0.1",
+ "uuid": "^8.3.2"
+ }
+ },
+ "@actions/github": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz",
+ "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==",
+ "requires": {
+ "@actions/http-client": "^2.2.0",
+ "@octokit/core": "^5.0.1",
+ "@octokit/plugin-paginate-rest": "^9.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^10.0.0"
+ },
+ "dependencies": {
+ "@octokit/openapi-types": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
+ "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="
+ },
+ "@octokit/plugin-paginate-rest": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz",
+ "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==",
+ "requires": {
+ "@octokit/types": "^12.6.0"
+ }
+ },
+ "@octokit/plugin-rest-endpoint-methods": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz",
+ "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==",
+ "requires": {
+ "@octokit/types": "^12.6.0"
+ }
+ },
+ "@octokit/types": {
+ "version": "12.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
+ "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
+ "requires": {
+ "@octokit/openapi-types": "^20.0.0"
+ }
+ }
+ }
+ },
+ "@actions/http-client": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz",
+ "integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==",
+ "requires": {
+ "tunnel": "^0.0.6",
+ "undici": "^5.25.4"
+ }
+ },
+ "@fastify/busboy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="
+ },
+ "@octokit/auth-token": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
+ "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="
+ },
+ "@octokit/core": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz",
+ "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==",
+ "requires": {
+ "@octokit/auth-token": "^4.0.0",
+ "@octokit/graphql": "^7.1.0",
+ "@octokit/request": "^8.3.1",
+ "@octokit/request-error": "^5.1.0",
+ "@octokit/types": "^13.0.0",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/endpoint": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz",
+ "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==",
+ "requires": {
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/graphql": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz",
+ "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==",
+ "requires": {
+ "@octokit/request": "^8.3.0",
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/openapi-types": {
+ "version": "22.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
+ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
+ },
+ "@octokit/plugin-paginate-rest": {
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz",
+ "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==",
+ "requires": {
+ "@octokit/types": "^13.5.0"
+ }
+ },
+ "@octokit/plugin-request-log": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.0.tgz",
+ "integrity": "sha512-FiGcyjdtYPlr03ExBk/0ysIlEFIFGJQAVoPPMxL19B24bVSEiZQnVGBunNtaAF1YnvE/EFoDpXmITtRnyCiypQ=="
+ },
+ "@octokit/plugin-rest-endpoint-methods": {
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.4.tgz",
+ "integrity": "sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==",
+ "requires": {
+ "@octokit/types": "^13.5.0"
+ }
+ },
+ "@octokit/request": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
+ "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
+ "requires": {
+ "@octokit/endpoint": "^9.0.1",
+ "@octokit/request-error": "^5.1.0",
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/request-error": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
+ "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
+ "requires": {
+ "@octokit/types": "^13.1.0",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "@octokit/rest": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.0.tgz",
+ "integrity": "sha512-XudXXOmiIjivdjNZ+fN71NLrnDM00sxSZlhqmPR3v0dVoJwyP628tSlc12xqn8nX3N0965583RBw5GPo6r8u4Q==",
+ "requires": {
+ "@octokit/core": "^6.1.2",
+ "@octokit/plugin-paginate-rest": "^11.0.0",
+ "@octokit/plugin-request-log": "^5.1.0",
+ "@octokit/plugin-rest-endpoint-methods": "^13.0.0"
+ },
+ "dependencies": {
+ "@octokit/auth-token": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz",
+ "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA=="
+ },
+ "@octokit/core": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
+ "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
+ "requires": {
+ "@octokit/auth-token": "^5.0.0",
+ "@octokit/graphql": "^8.0.0",
+ "@octokit/request": "^9.0.0",
+ "@octokit/request-error": "^6.0.1",
+ "@octokit/types": "^13.0.0",
+ "before-after-hook": "^3.0.2",
+ "universal-user-agent": "^7.0.0"
+ }
+ },
+ "@octokit/endpoint": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
+ "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
+ "requires": {
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^7.0.2"
+ }
+ },
+ "@octokit/graphql": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
+ "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
+ "requires": {
+ "@octokit/request": "^9.0.0",
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^7.0.0"
+ }
+ },
+ "@octokit/request": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.1.tgz",
+ "integrity": "sha512-pyAguc0p+f+GbQho0uNetNQMmLG1e80WjkIaqqgUkihqUp0boRU6nKItXO4VWnr+nbZiLGEyy4TeKRwqaLvYgw==",
+ "requires": {
+ "@octokit/endpoint": "^10.0.0",
+ "@octokit/request-error": "^6.0.1",
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^7.0.2"
+ }
+ },
+ "@octokit/request-error": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.1.tgz",
+ "integrity": "sha512-1mw1gqT3fR/WFvnoVpY/zUM2o/XkMs/2AszUUG9I69xn0JFLv6PGkPhNk5lbfvROs79wiS0bqiJNxfCZcRJJdg==",
+ "requires": {
+ "@octokit/types": "^13.0.0"
+ }
+ },
+ "before-after-hook": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="
+ },
+ "universal-user-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+ "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
+ }
+ }
+ },
+ "@octokit/types": {
+ "version": "13.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz",
+ "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==",
+ "requires": {
+ "@octokit/openapi-types": "^22.2.0"
+ }
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "axios": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "requires": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "beauty-html": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/beauty-html/-/beauty-html-1.3.1.tgz",
+ "integrity": "sha512-c0iKWc527T2MQcYhIMMw9OHN8kcXSf/ijadWzURhZWi6e6cnBXxAQ5IlXbYd0YZJE9lFtXRB1fJVQrvJf5DmPQ=="
+ },
+ "before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+ },
+ "deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
+ "diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="
+ },
+ "follow-redirects": {
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "semver": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="
+ },
+ "tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
+ },
+ "undici": {
+ "version": "5.28.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
+ "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
+ "requires": {
+ "@fastify/busboy": "^2.0.0"
+ }
+ },
+ "universal-user-agent": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
+ },
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "xmldom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
+ "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="
+ }
+ }
+}
diff --git a/.github/actions/update-version-durability/package.json b/.github/actions/update-version-durability/package.json
new file mode 100644
index 000000000000..2a6658581540
--- /dev/null
+++ b/.github/actions/update-version-durability/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "scripts",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@actions/core": "^1.10.1",
+ "@octokit/rest": "^21.0.0",
+ "axios": "^1.7.2",
+ "beauty-html": "^1.3.1",
+ "colors": "^1.4.0",
+ "diff": "^5.1.0",
+ "semver": "^7.5.4",
+ "xmldom": "^0.6.0"
+ }
+}
diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml
index b46c124d149b..378769883f19 100644
--- a/.github/workflows/ci-test-e2e.yml
+++ b/.github/workflows/ci-test-e2e.yml
@@ -67,6 +67,8 @@ on:
required: false
CODECOV_TOKEN:
required: false
+ REPORTER_JIRA_ROCKETCHAT_API_KEY:
+ required: false
env:
MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true
@@ -122,7 +124,7 @@ jobs:
# 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.pull_request.head.repo.full_name != github.repository
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
with:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
@@ -176,6 +178,12 @@ 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
@@ -210,6 +218,8 @@ jobs:
sleep 10
done;
+ - name: Remove unused Docker images
+ run: docker system prune -af
- name: E2E Test API
if: inputs.type == 'api'
working-directory: ./apps/meteor
@@ -250,10 +260,15 @@ jobs:
IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }}
REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }}
REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }}
+ REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }}
REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }}
REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }}
REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }}
REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }}
+ REPORTER_ROCKETCHAT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ REPORTER_ROCKETCHAT_AUTHOR: ${{ github.event.pull_request.user.login }}
+ REPORTER_ROCKETCHAT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ REPORTER_ROCKETCHAT_PR: ${{ github.event.pull_request.number }}
QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }}
QASE_REPORT: ${{ github.ref == 'refs/heads/develop' && 'true' || '' }}
CI: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b542cfbf6523..411aa2cc5b1a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,10 +35,12 @@ jobs:
# to avoid this, we are using a dummy license, expiring at 2025-06-31
enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU=
steps:
- - uses: Bhacaz/checkout-files@v2
+ - uses: actions/checkout@v4
with:
- files: package.json
- branch: ${{ github.ref }}
+ sparse-checkout: |
+ package.json
+ sparse-checkout-cone-mode: false
+ ref: ${{ github.ref }}
- id: var
run: |
@@ -85,10 +87,12 @@ jobs:
runs-on: ubuntu-20.04
needs: [release-versions]
steps:
- - uses: Bhacaz/checkout-files@v2
+ - uses: actions/checkout@v4
with:
- files: package.json
- branch: ${{ github.ref }}
+ sparse-checkout: |
+ package.json
+ sparse-checkout-cone-mode: false
+ ref: ${{ github.ref }}
- name: Register release on cloud as Draft
if: github.event_name == 'release'
@@ -350,6 +354,7 @@ jobs:
QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }}
REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }}
REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }}
+ REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }}
test-api-ee:
name: 🔨 Test API (EE)
@@ -401,6 +406,7 @@ jobs:
REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }}
REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }}
test-ui-ee-no-watcher:
name: 🔨 Test UI (EE)
@@ -431,15 +437,44 @@ jobs:
REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }}
REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }}
tests-done:
name: ✅ Tests Done
runs-on: ubuntu-20.04
needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-ui-ee-no-watcher]
-
+ if: always()
steps:
- name: Test finish aggregation
run: |
+ if [[ '${{ needs.checks.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-unit.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-api.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-ui.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-api-ee.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-ui-ee.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
+ if [[ '${{ needs.test-ui-ee-no-watcher.result }}' != 'success' ]]; then
+ exit 1
+ fi
+
echo finished
deploy:
@@ -449,10 +484,12 @@ jobs:
needs: [build-gh-docker, release-versions]
steps:
- - uses: Bhacaz/checkout-files@v2
+ - uses: actions/checkout@v4
with:
- files: package.json
- branch: ${{ github.ref }}
+ sparse-checkout: |
+ package.json
+ sparse-checkout-cone-mode: false
+ ref: ${{ github.ref }}
- name: Restore build
uses: actions/download-artifact@v3
@@ -732,10 +769,12 @@ jobs:
- docker-image-publish
- release-versions
steps:
- - uses: Bhacaz/checkout-files@v2
+ - uses: actions/checkout@v4
with:
- files: package.json
- branch: ${{ github.ref }}
+ sparse-checkout: |
+ package.json
+ sparse-checkout-cone-mode: false
+ ref: ${{ github.ref }}
- name: Releases service
env:
@@ -784,10 +823,15 @@ jobs:
repository: RocketChat/Release.Distributions
client-payload: '{"tag": "${{ github.ref_name }}"}'
- - name: Update docs
- uses: peter-evans/repository-dispatch@v2
- with:
- token: ${{ secrets.DOCS_PAT }}
- event-type: new_release
- repository: RocketChat/docs
- client-payload: '{"tag": "${{ github.ref_name }}"}'
+ docs-update:
+ name: Update Version Durability
+
+ if: github.event_name == 'release'
+ needs:
+ - services-docker-image-publish
+ - docker-image-publish
+
+ uses: ./.github/workflows/update-version-durability.yml
+ secrets:
+ CI_PAT: ${{ secrets.CI_PAT }}
+ D360_TOKEN: ${{ secrets.D360_TOKEN }}
diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml
new file mode 100644
index 000000000000..e52b4870b369
--- /dev/null
+++ b/.github/workflows/update-version-durability.yml
@@ -0,0 +1,35 @@
+name: Update Version Durability
+
+on:
+ workflow_dispatch:
+ workflow_call:
+ secrets:
+ CI_PAT:
+ required: true
+ D360_TOKEN:
+ required: true
+
+jobs:
+ update-versions:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js
+ uses: actions/setup-node@v3.7.0
+ with:
+ node-version: '20.15.1'
+
+ - name: Install dependencies
+ run: |
+ cd ./.github/actions/update-version-durability
+ npm install
+
+ - name: Update Version Durability
+ uses: ./.github/actions/update-version-durability
+ with:
+ GH_TOKEN: ${{ secrets.CI_PAT }}
+ D360_TOKEN: ${{ secrets.D360_TOKEN }}
+ D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb
+ PUBLISH: true
diff --git a/README.md b/README.md
index 64dec811e1ca..56e38c111e97 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,10 @@ yarn dsv # run only meteor (front and back) with pre-built packages
After initialized, you can access the server at http://localhost:3000
+More details at: [Developer Docs](https://developer.rocket.chat/v1/docs/server-environment-setup)
+PS: For Windows you MUST use WSL2 and have +12Gb RAM
+
+
# Gitpod Setup
1. Click the button below to open this project in Gitpod.
diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore
index 2bbdbae00b89..2701a871d981 100644
--- a/apps/meteor/.eslintignore
+++ b/apps/meteor/.eslintignore
@@ -1,6 +1,5 @@
/node_modules/
#/tests/e2e/
-/tests/data/
/packages/
/app/emoji-emojione/generateEmojiIndex.js
/public/
diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js
index eca1284e62e5..b73a24a275e4 100644
--- a/apps/meteor/.mocharc.api.js
+++ b/apps/meteor/.mocharc.api.js
@@ -1,13 +1,14 @@
'use strict';
-/**
+/*
* Mocha configuration for REST API integration tests.
*/
-module.exports = {
+module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({
...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916
timeout: 10000,
bail: true,
- file: 'tests/end-to-end/teardown.js',
+ retries: 0,
+ file: 'tests/end-to-end/teardown.ts',
spec: ['tests/end-to-end/api/**/*', 'tests/end-to-end/apps/*'],
-};
+});
diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md
index f17422bbb318..4f6de9c113b7 100644
--- a/apps/meteor/CHANGELOG.md
+++ b/apps/meteor/CHANGELOG.md
@@ -1,11 +1,333 @@
# @rocket.chat/meteor
-## 6.10.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
- Bump @rocket.chat/meteor version.
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.2
+ - @rocket.chat/rest-typings@6.11.0-rc.2
+ - @rocket.chat/api-client@0.2.3-rc.2
+ - @rocket.chat/license@0.2.3-rc.2
+ - @rocket.chat/omnichannel-services@0.3.0-rc.2
+ - @rocket.chat/pdf-worker@0.2.0-rc.2
+ - @rocket.chat/presence@0.2.3-rc.2
+ - @rocket.chat/apps@0.1.3-rc.2
+ - @rocket.chat/core-services@0.5.0-rc.2
+ - @rocket.chat/cron@0.1.3-rc.2
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.2
+ - @rocket.chat/gazzodown@9.0.0-rc.2
+ - @rocket.chat/model-typings@0.6.0-rc.2
+ - @rocket.chat/ui-contexts@9.0.0-rc.2
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.2
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.2
+ - @rocket.chat/ui-client@9.0.0-rc.2
+ - @rocket.chat/ui-video-conf@9.0.0-rc.2
+ - @rocket.chat/web-ui-registration@9.0.0-rc.2
+ - @rocket.chat/instance-status@0.1.3-rc.2
+
+
+## 6.11.0-rc.1
+
+### Patch Changes
+
+- Bump @rocket.chat/meteor version.
+
+- Updated dependencies []:
+
+ - @rocket.chat/core-typings@6.11.0-rc.1
+ - @rocket.chat/rest-typings@6.11.0-rc.1
+ - @rocket.chat/api-client@0.2.2-rc.1
+ - @rocket.chat/license@0.2.2-rc.1
+ - @rocket.chat/omnichannel-services@0.3.0-rc.1
+ - @rocket.chat/pdf-worker@0.2.0-rc.1
+ - @rocket.chat/presence@0.2.2-rc.1
+ - @rocket.chat/apps@0.1.2-rc.1
+ - @rocket.chat/core-services@0.5.0-rc.1
+ - @rocket.chat/cron@0.1.2-rc.1
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.1
+ - @rocket.chat/gazzodown@9.0.0-rc.1
+ - @rocket.chat/model-typings@0.6.0-rc.1
+ - @rocket.chat/ui-contexts@9.0.0-rc.1
+ - @rocket.chat/server-cloud-communication@0.0.2
+ - @rocket.chat/models@0.2.0-rc.1
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.1
+ - @rocket.chat/ui-client@9.0.0-rc.1
+ - @rocket.chat/ui-video-conf@9.0.0-rc.1
+ - @rocket.chat/web-ui-registration@9.0.0-rc.1
+ - @rocket.chat/instance-status@0.1.2-rc.1
+
+
+## 6.11.0-rc.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
+
+- ([#32570](https://github.com/RocketChat/Rocket.Chat/pull/32570)) Login services button was not respecting the button color and text color settings. Implemented a fix to respect these settings and change the button colors accordingly.
+
+ Added a warning on all settings which allow admins to change OAuth button colors, so that they can be alerted about WCAG (Web Content Accessibility Guidelines) compliance.
+
+- ([#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
+
+- ([#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, 15664127be, 25da5280a5, 1b7b1161cf, 439faa87d3, 03c8b066f9, 2d89a0c448, 439faa87d3, 24f7df4894, 3ffe4a2944, 3b4b19cfc5, 4e8aa575a6, 03c8b066f9, 264d7d5496, b8e5887fb9]:
+
+ - @rocket.chat/fuselage-ui-kit@9.0.0-rc.0
+ - @rocket.chat/i18n@0.6.0-rc.0
+ - @rocket.chat/tools@0.2.2-rc.0
+ - @rocket.chat/web-ui-registration@9.0.0-rc.0
+ - @rocket.chat/ui-client@9.0.0-rc.0
+ - @rocket.chat/model-typings@0.6.0-rc.0
+ - @rocket.chat/omnichannel-services@0.3.0-rc.0
+ - @rocket.chat/pdf-worker@0.2.0-rc.0
+ - @rocket.chat/core-services@0.5.0-rc.0
+ - @rocket.chat/ui-video-conf@9.0.0-rc.0
+ - @rocket.chat/core-typings@6.11.0-rc.0
+ - @rocket.chat/ui-contexts@9.0.0-rc.0
+ - @rocket.chat/models@0.2.0-rc.0
+ - @rocket.chat/ui-kit@0.36.0-rc.0
+ - @rocket.chat/rest-typings@6.11.0-rc.0
+ - @rocket.chat/apps@0.1.2-rc.0
+ - @rocket.chat/presence@0.2.2-rc.0
+ - @rocket.chat/gazzodown@9.0.0-rc.0
+ - @rocket.chat/api-client@0.2.2-rc.0
+ - @rocket.chat/license@0.2.2-rc.0
+ - @rocket.chat/cron@0.1.2-rc.0
+ - @rocket.chat/ui-theming@0.2.0
+ - @rocket.chat/ui-avatar@5.0.0-rc.0
+ - @rocket.chat/instance-status@0.1.2-rc.0
+ - @rocket.chat/server-cloud-communication@0.0.2
+
+## 6.10.2
+
+### Patch Changes
+
- Bump @rocket.chat/meteor version.
- ([#32935](https://github.com/RocketChat/Rocket.Chat/pull/32935)) Fixed an issue that prevented apps from being updated or uninstalled in some cases
diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts
index 85fc0658542d..3136a6c16e13 100644
--- a/apps/meteor/app/api/server/lib/getUploadFormData.ts
+++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts
@@ -63,7 +63,7 @@ export async function getUploadFormData<
function onFile(
fieldname: string,
file: Readable & { truncated: boolean },
- { filename, encoding }: { filename: string; encoding: string },
+ { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string },
) {
if (options.field && fieldname !== options.field) {
file.resume();
@@ -85,7 +85,7 @@ export async function getUploadFormData<
file,
filename,
encoding,
- mimetype: getMimeType(filename),
+ mimetype: getMimeType(mimetype, filename),
fieldname,
fields,
fileBuffer: Buffer.concat(fileChunks),
diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts
index 410a65fe7eda..7ae585b89dfa 100644
--- a/apps/meteor/app/api/server/v1/users.ts
+++ b/apps/meteor/app/api/server/v1/users.ts
@@ -19,6 +19,7 @@ import {
isUsersCheckUsernameAvailabilityParamsGET,
isUsersSendConfirmationEmailParamsPOST,
} from '@rocket.chat/rest-typings';
+import { getLoginExpirationInMs } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -1065,8 +1066,9 @@ API.v1.addRoute(
const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken);
- const tokenExpires =
- (token && 'when' in token && new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000)) || undefined;
+ const loginExp = settings.get('Accounts_LoginExpiration');
+
+ const tokenExpires = (token && 'when' in token && new Date(token.when.getTime() + getLoginExpirationInMs(loginExp))) || undefined;
return API.v1.success({
token: xAuthToken,
@@ -1214,7 +1216,7 @@ API.v1.addRoute(
throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
}
- void notifyOnUserChange({ clientAction: 'updated', id: this.userId, diff: { 'services.resume.loginTokens': [] } });
+ void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [] } });
return API.v1.success({
message: `User ${userId} has been logged out!`,
diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js
index ab2632c912b0..13db1179310c 100644
--- a/apps/meteor/app/apps/server/bridges/listeners.js
+++ b/apps/meteor/app/apps/server/bridges/listeners.js
@@ -143,10 +143,11 @@ export class AppListenerBridge {
};
case AppInterface.IPreRoomUserLeave:
case AppInterface.IPostRoomUserLeave:
- const [leavingUser] = payload;
+ const [leavingUser, removedBy] = payload;
return {
room: rm,
leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser),
+ removedBy: this.orch.getConverters().get('users').convertToApp(removedBy),
};
default:
return rm;
diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts
index 7067ab8e6a52..ec5cff29a99b 100644
--- a/apps/meteor/app/apps/server/bridges/livechat.ts
+++ b/apps/meteor/app/apps/server/bridges/livechat.ts
@@ -7,14 +7,18 @@ import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/Livechat
import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models';
-import { Random } from '@rocket.chat/random';
import { callbacks } from '../../../../lib/callbacks';
import { deasyncPromise } from '../../../../server/deasync/deasync';
-import { getRoom } from '../../../livechat/server/api/lib/livechat';
import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped';
import { settings } from '../../../settings/server';
+declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' {
+ interface IExtraRoomParams {
+ customFields?: Record;
+ }
+}
+
export class AppLivechatBridge extends LivechatBridge {
constructor(private readonly orch: IAppServerOrchestrator) {
super();
@@ -79,17 +83,14 @@ export class AppLivechatBridge extends LivechatBridge {
await LivechatTyped.updateMessage(data);
}
- protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise {
+ protected async createRoom(
+ visitor: IVisitor,
+ agent: IUser,
+ appId: string,
+ { source, customFields }: IExtraRoomParams = {},
+ ): Promise {
this.orch.debugLog(`The App ${appId} is creating a livechat room.`);
- const { source } = extraParams || {};
- // `source` will likely have the properties below, so we tell TS it's alright
- const { sidebarIcon, defaultIcon, label } = (source || {}) as {
- sidebarIcon?: string;
- defaultIcon?: string;
- label?: string;
- };
-
let agentRoom: SelectedAgent | undefined;
if (agent?.id) {
const user = await Users.getAgentInfo(agent.id, settings.get('Livechat_show_agent_email'));
@@ -99,25 +100,27 @@ export class AppLivechatBridge extends LivechatBridge {
agentRoom = { agentId: user._id, username: user.username };
}
- const result = await getRoom({
- guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor),
- agent: agentRoom,
- rid: Random.id(),
+ const room = await LivechatTyped.createRoom({
+ visitor: this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor),
roomInfo: {
source: {
type: OmnichannelSourceType.APP,
id: appId,
alias: this.orch.getManager()?.getOneById(appId)?.getName(),
- label,
- sidebarIcon,
- defaultIcon,
+ ...(source &&
+ source.type === 'app' && {
+ sidebarIcon: source.sidebarIcon,
+ defaultIcon: source.defaultIcon,
+ label: source.label,
+ }),
},
},
- extraParams: undefined,
+ agent: agentRoom,
+ extraData: customFields && { customFields },
});
// #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
- return this.orch.getConverters()?.get('rooms').convertRoom(result.room) as Promise;
+ return this.orch.getConverters()?.get('rooms').convertRoom(room) as Promise;
}
protected async closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise {
@@ -195,7 +198,33 @@ export class AppLivechatBridge extends LivechatBridge {
...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }),
};
- return LivechatTyped.registerGuest(registerData);
+ const livechatVisitor = await LivechatTyped.registerGuest(registerData);
+
+ if (!livechatVisitor) {
+ throw new Error('Invalid visitor, cannot create');
+ }
+
+ return livechatVisitor._id;
+ }
+
+ protected async createAndReturnVisitor(visitor: IVisitor, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is creating a livechat visitor.`);
+
+ const registerData = {
+ department: visitor.department,
+ username: visitor.username,
+ name: visitor.name,
+ token: visitor.token,
+ email: '',
+ connectionData: undefined,
+ id: visitor.id,
+ ...(visitor.phone?.length && { phone: { number: visitor.phone[0].phoneNumber } }),
+ ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }),
+ };
+
+ const livechatVisitor = await LivechatTyped.registerGuest(registerData);
+
+ return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor);
}
protected async transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise {
@@ -217,7 +246,8 @@ export class AppLivechatBridge extends LivechatBridge {
username,
name,
type,
- };
+ userType: 'user',
+ } as const;
let userId;
let transferredTo;
diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts
index bbd24152716f..344acc74bda4 100644
--- a/apps/meteor/app/apps/server/bridges/rooms.ts
+++ b/apps/meteor/app/apps/server/bridges/rooms.ts
@@ -1,16 +1,19 @@
import type { IAppServerOrchestrator } from '@rocket.chat/apps';
-import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
+import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages';
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
+import type { GetMessagesOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
-import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom } from '@rocket.chat/core-typings';
-import { Subscriptions, Users, Rooms } from '@rocket.chat/models';
+import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings';
+import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models';
+import type { FindOptions, Sort } from 'mongodb';
import { createDirectMessage } from '../../../../server/methods/createDirectMessage';
import { createDiscussion } from '../../../discussion/server/methods/createDiscussion';
import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
import { deleteRoom } from '../../../lib/server/functions/deleteRoom';
+import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom';
import { createChannelMethod } from '../../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup';
@@ -102,6 +105,38 @@ export class AppRoomBridge extends RoomBridge {
return this.orch.getConverters()?.get('users').convertById(room.u._id);
}
+ protected async getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is getting the messages of the room: "${roomId}" with options:`, options);
+
+ const { limit, skip = 0, sort: _sort } = options;
+
+ const messageConverter = this.orch.getConverters()?.get('messages');
+ if (!messageConverter) {
+ throw new Error('Message converter not found');
+ }
+
+ // We support only one field for now
+ const sort: Sort | undefined = _sort?.createdAt ? { ts: _sort.createdAt } : undefined;
+
+ const messageQueryOptions: FindOptions = {
+ limit,
+ skip,
+ sort,
+ };
+
+ const query = {
+ rid: roomId,
+ _hidden: { $ne: true },
+ t: { $exists: false },
+ };
+
+ const cursor = Messages.find(query, messageQueryOptions);
+
+ const messagePromises: Promise[] = await cursor.map((message) => messageConverter.convertMessageRaw(message)).toArray();
+
+ return Promise.all(messagePromises);
+ }
+
protected async getMembers(roomId: string, appId: string): Promise> {
this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`);
const subscriptions = await Subscriptions.findByRoomId(roomId, {});
@@ -209,4 +244,14 @@ export class AppRoomBridge extends RoomBridge {
const userConverter = this.orch.getConverters().get('users');
return users.map((user: ICoreUser) => userConverter.convertToApp(user));
}
+
+ protected async removeUsers(roomId: string, usernames: Array, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is removing users ${usernames} from room id: ${roomId}`);
+ if (!roomId) {
+ throw new Error('roomId was not provided.');
+ }
+
+ const members = await Users.findUsersByUsernames(usernames, { limit: 50 }).toArray();
+ await Promise.all(members.map((user) => removeUserFromRoom(roomId, user)));
+ }
}
diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts
index bebcb25a6f51..efab0f201f87 100644
--- a/apps/meteor/app/apps/server/bridges/videoConferences.ts
+++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts
@@ -59,6 +59,10 @@ export class AppVideoConferenceBridge extends VideoConferenceBridge {
if (data.status > oldData.status) {
await VideoConf.setStatus(call._id, data.status);
}
+
+ if (data.discussionRid !== oldData.discussionRid) {
+ await VideoConf.assignDiscussionToConference(call._id, data.discussionRid);
+ }
}
protected async registerProvider(info: IVideoConfProvider, appId: string): Promise {
diff --git a/apps/meteor/app/apps/server/converters/cachedFunction.ts b/apps/meteor/app/apps/server/converters/cachedFunction.ts
new file mode 100644
index 000000000000..3310574f0160
--- /dev/null
+++ b/apps/meteor/app/apps/server/converters/cachedFunction.ts
@@ -0,0 +1,17 @@
+export const cachedFunction = any>(fn: F) => {
+ const cache = new Map();
+
+ return ((...args) => {
+ const cacheKey = JSON.stringify(args);
+
+ if (cache.has(cacheKey)) {
+ return cache.get(cacheKey) as ReturnType;
+ }
+
+ const result = fn(...args);
+
+ cache.set(cacheKey, result);
+
+ return result;
+ }) as F;
+};
diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js
index 187a6519339a..d7dae512e9a8 100644
--- a/apps/meteor/app/apps/server/converters/messages.js
+++ b/apps/meteor/app/apps/server/converters/messages.js
@@ -1,9 +1,13 @@
+import { isMessageFromVisitor } from '@rocket.chat/core-typings';
import { Messages, Rooms, Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
+import { cachedFunction } from './cachedFunction';
import { transformMappedData } from './transformMappedData';
export class AppMessagesConverter {
+ mem = new WeakMap();
+
constructor(orch) {
this.orch = orch;
}
@@ -14,11 +18,54 @@ export class AppMessagesConverter {
return this.convertMessage(msg);
}
+ async convertMessageRaw(msgObj) {
+ if (!msgObj) {
+ return undefined;
+ }
+
+ const { attachments, ...message } = msgObj;
+ const getAttachments = async () => this._convertAttachmentsToApp(attachments);
+
+ const map = {
+ id: '_id',
+ threadId: 'tmid',
+ reactions: 'reactions',
+ parseUrls: 'parseUrls',
+ text: 'msg',
+ createdAt: 'ts',
+ updatedAt: '_updatedAt',
+ editedAt: 'editedAt',
+ emoji: 'emoji',
+ avatarUrl: 'avatar',
+ alias: 'alias',
+ file: 'file',
+ customFields: 'customFields',
+ groupable: 'groupable',
+ token: 'token',
+ blocks: 'blocks',
+ roomId: 'rid',
+ editor: 'editedBy',
+ attachments: getAttachments,
+ sender: 'u',
+ };
+
+ return transformMappedData(message, map);
+ }
+
async convertMessage(msgObj) {
if (!msgObj) {
return undefined;
}
+ const cache =
+ this.mem.get(msgObj) ??
+ 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')))],
+ ]);
+
+ this.mem.set(msgObj, cache);
+
const map = {
id: '_id',
threadId: 'tmid',
@@ -37,7 +84,7 @@ export class AppMessagesConverter {
token: 'token',
blocks: 'blocks',
room: async (message) => {
- const result = await this.orch.getConverters().get('rooms').convertById(message.rid);
+ const result = await cache.get('room')(message.rid);
delete message.rid;
return result;
},
@@ -49,7 +96,7 @@ export class AppMessagesConverter {
return undefined;
}
- return this.orch.getConverters().get('users').convertById(editedBy._id);
+ return cache.get('user')(editedBy._id);
},
attachments: async (message) => {
const result = await this._convertAttachmentsToApp(message.attachments);
@@ -61,16 +108,19 @@ export class AppMessagesConverter {
return undefined;
}
- let user = await this.orch.getConverters().get('users').convertById(message.u._id);
-
- // When the sender of the message is a Guest (livechat) and not a user
- if (!user) {
- user = this.orch.getConverters().get('users').convertToApp(message.u);
- }
+ // 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));
delete message.u;
- return user;
+ /**
+ * Old System Messages from visitor doesn't have the `token` field, to not return
+ * `sender` as undefined, so we need to add this fallback here.
+ */
+
+ return user || this.orch.getConverters().get('users').convertToApp(message.u);
},
};
diff --git a/apps/meteor/app/apps/server/converters/threads.ts b/apps/meteor/app/apps/server/converters/threads.ts
index 840f4f1613eb..e31ee094b4d7 100644
--- a/apps/meteor/app/apps/server/converters/threads.ts
+++ b/apps/meteor/app/apps/server/converters/threads.ts
@@ -5,6 +5,7 @@ import type { IUser } from '@rocket.chat/core-typings';
import { isEditedMessage, type IMessage } from '@rocket.chat/core-typings';
import { Messages } from '@rocket.chat/models';
+import { cachedFunction } from './cachedFunction';
import { transformMappedData } from './transformMappedData';
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -18,24 +19,6 @@ interface Orchestrator {
};
}
-const cachedFunction = any>(fn: F) => {
- const cache = new Map();
-
- return ((...args) => {
- const cacheKey = JSON.stringify(args);
-
- if (cache.has(cacheKey)) {
- return cache.get(cacheKey) as ReturnType;
- }
-
- const result = fn(...args);
-
- cache.set(cacheKey, result);
-
- return result;
- }) as F;
-};
-
export class AppThreadsConverter implements IAppThreadsConverter {
constructor(
private readonly orch: {
diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js
index bffbe1f9876d..2e4c599ce558 100644
--- a/apps/meteor/app/authentication/server/startup/index.js
+++ b/apps/meteor/app/authentication/server/startup/index.js
@@ -2,6 +2,7 @@ import { Apps, AppEvents } from '@rocket.chat/apps';
import { User } from '@rocket.chat/core-services';
import { Roles, Settings, Users } from '@rocket.chat/models';
import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers';
+import { getLoginExpirationInDays } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -31,7 +32,7 @@ Accounts.config({
Meteor.startup(() => {
settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => {
- Accounts._options.loginExpirationInDays = settings.get('Accounts_LoginExpiration');
+ Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration'));
Accounts.emailTemplates.siteName = settings.get('Site_Name');
diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts
index 3ad61c4c42f0..ecf014248830 100644
--- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts
+++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts
@@ -44,7 +44,7 @@ Meteor.startup(() => {
subscription,
user,
}) {
- if (drid || !Number.isNaN(dcount)) {
+ if (drid || !Number.isNaN(Number(dcount))) {
return false;
}
if (!subscription) {
diff --git a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
index 0f42f495e962..d8e3637575ab 100644
--- a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
+++ b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts
@@ -1,5 +1,5 @@
import type { IRoom } from '@rocket.chat/core-typings';
-import { Messages, Rooms } from '@rocket.chat/models';
+import { Messages, Rooms, VideoConference } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages';
@@ -108,6 +108,8 @@ callbacks.add(
},
},
);
+
+ await VideoConference.unsetDiscussionRid(drid);
return drid;
},
callbacks.priority.LOW,
diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts
index 0cc344ff5152..bbd6f208f35a 100644
--- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts
+++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts
@@ -257,7 +257,7 @@ class E2E extends Emitter {
return null;
}
- if (room.encrypted !== true && !room.e2eKeyId) {
+ if (!room.encrypted) {
return null;
}
@@ -272,7 +272,7 @@ class E2E extends Emitter {
delete this.instancesByRoomId[rid];
}
- async persistKeys(
+ private async persistKeys(
{ public_key, private_key }: KeyPair,
password: string,
{ force }: { force: boolean } = { force: false },
diff --git a/apps/meteor/app/emoji/client/emojiParser.js b/apps/meteor/app/emoji/client/emojiParser.js
index 7b887bb0575f..0b3b722aaebd 100644
--- a/apps/meteor/app/emoji/client/emojiParser.js
+++ b/apps/meteor/app/emoji/client/emojiParser.js
@@ -1,17 +1,13 @@
import { isIE11 } from '../../../client/lib/utils/isIE11';
import { emoji } from './lib';
-/*
+/**
* emojiParser is a function that will replace emojis
- * @param {Object} message - The message object
+ * @param {{ html: string }} message - The message object
+ * @return {{ html: string }}
*/
-
-const emojiParser = (message) => {
- if (!message.html?.trim()) {
- return message;
- }
-
- let html = message.html.trim();
+export const emojiParser = ({ html }) => {
+ html = html.trim();
// ' to apostrophe (') for emojis such as :')
html = html.replace(/'/g, "'");
@@ -64,7 +60,5 @@ const emojiParser = (message) => {
// line breaks '
' back to '
'
html = html.replace(/
/g, '
');
- return { ...message, html };
+ return { html };
};
-
-export { emojiParser };
diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
index 57ea20f00cb1..b6ffc0ca4629 100644
--- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts
+++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
@@ -15,9 +15,15 @@ import { notifyOnRoomChangedById } from '../lib/notifyListener';
export const addUserToRoom = async function (
rid: string,
- user: Pick | string,
+ user: Pick | string,
inviter?: Pick,
- silenced?: boolean,
+ {
+ skipSystemMessage,
+ skipAlertSound,
+ }: {
+ skipSystemMessage?: boolean;
+ skipAlertSound?: boolean;
+ } = {},
): Promise {
const now = new Date();
const room = await Rooms.findOneById(rid);
@@ -43,12 +49,12 @@ export const addUserToRoom = async function (
}
try {
- await callbacks.run('federation.beforeAddUserToARoom', { user, inviter }, room);
+ await callbacks.run('federation.beforeAddUserToARoom', { user: userToBeAdded, inviter }, room);
} catch (error) {
throw new Meteor.Error((error as any)?.message);
}
- await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter: userToBeAdded });
+ await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter });
// Check if user is already in room
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id);
@@ -79,7 +85,7 @@ export const addUserToRoom = async function (
await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, {
ts: now,
open: true,
- alert: true,
+ alert: !skipAlertSound,
unread: 1,
userMentions: 1,
groupMentions: 0,
@@ -93,7 +99,7 @@ export const addUserToRoom = async function (
throw new Meteor.Error('error-invalid-user', 'Cannot add an user to a room without a username');
}
- if (!silenced) {
+ if (!skipSystemMessage) {
if (inviter) {
const extraData = {
ts: now,
diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
index 3b065c68f15c..c55ee382f10c 100644
--- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
+++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
@@ -10,11 +10,7 @@ import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRo
import { settings } from '../../../settings/server';
import { notifyOnRoomChangedById } from '../lib/notifyListener';
-export const removeUserFromRoom = async function (
- rid: string,
- user: IUser,
- options?: { byUser: Pick },
-): Promise {
+export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise {
const room = await Rooms.findOneById(rid);
if (!room) {
@@ -22,7 +18,7 @@ export const removeUserFromRoom = async function (
}
try {
- await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user);
+ await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser);
} catch (error: any) {
if (error.name === AppsEngineException.name) {
throw new Meteor.Error('error-app-prevented', error.message);
@@ -75,5 +71,5 @@ export const removeUserFromRoom = async function (
void notifyOnRoomChangedById(rid);
- await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user);
+ await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user, options?.byUser);
};
diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts
index f4e948390c99..635c236fda27 100644
--- a/apps/meteor/app/lib/server/lib/notifyListener.ts
+++ b/apps/meteor/app/lib/server/lib/notifyListener.ts
@@ -34,415 +34,322 @@ import {
type ClientAction = 'inserted' | 'updated' | 'removed';
-export async function notifyOnLivechatPriorityChanged(
- data: Pick,
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const { _id, ...rest } = data;
-
- void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } });
-}
-
-export async function notifyOnRoomChanged(
- data: T | T[],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const items = Array.isArray(data) ? data : [data];
-
- for (const item of items) {
- void api.broadcast('watch.rooms', { clientAction, room: item });
- }
+function withDbWatcherCheck Promise>(fn: T): T {
+ return dbWatchersDisabled ? fn : ((() => Promise.resolve()) as T);
}
-export async function notifyOnRoomChangedById(
- ids: T['_id'] | T['_id'][],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const eligibleIds = Array.isArray(ids) ? ids : [ids];
-
- const items = Rooms.findByIds(eligibleIds);
-
- for await (const item of items) {
- void api.broadcast('watch.rooms', { clientAction, room: item });
- }
-}
-
-export async function notifyOnRoomChangedByUsernamesOrUids(
- uids: T['u']['_id'][],
- usernames: T['u']['username'][],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const items = Rooms.findByUsernamesOrUids(uids, usernames);
-
- for await (const item of items) {
- void api.broadcast('watch.rooms', { clientAction, room: item });
- }
-}
-
-export async function notifyOnRoomChangedByUserDM(
- userId: T['u']['_id'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const items = Rooms.findDMsByUids([userId]);
-
- for await (const item of items) {
- void api.broadcast('watch.rooms', { clientAction, room: item });
- }
-}
-
-export async function notifyOnPermissionChanged(permission: IPermission, clientAction: ClientAction = 'updated'): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('permission.changed', { clientAction, data: permission });
+export const notifyOnLivechatPriorityChanged = withDbWatcherCheck(
+ async (data: Pick, clientAction: ClientAction = 'updated'): Promise => {
+ const { _id, ...rest } = data;
+ void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } });
+ },
+);
+
+export const notifyOnRoomChanged = withDbWatcherCheck(
+ async (data: T | T[], clientAction: ClientAction = 'updated'): Promise => {
+ const items = Array.isArray(data) ? data : [data];
+ for (const item of items) {
+ void api.broadcast('watch.rooms', { clientAction, room: item });
+ }
+ },
+);
+
+export const notifyOnRoomChangedById = withDbWatcherCheck(
+ async (ids: T['_id'] | T['_id'][], clientAction: ClientAction = 'updated'): Promise => {
+ const eligibleIds = Array.isArray(ids) ? ids : [ids];
+ const items = Rooms.findByIds(eligibleIds);
+ for await (const item of items) {
+ void api.broadcast('watch.rooms', { clientAction, room: item });
+ }
+ },
+);
+
+export const notifyOnRoomChangedByUsernamesOrUids = withDbWatcherCheck(
+ async (
+ uids: T['u']['_id'][],
+ usernames: T['u']['username'][],
+ clientAction: ClientAction = 'updated',
+ ): Promise => {
+ const items = Rooms.findByUsernamesOrUids(uids, usernames);
+ for await (const item of items) {
+ void api.broadcast('watch.rooms', { clientAction, room: item });
+ }
+ },
+);
+
+export const notifyOnRoomChangedByUserDM = withDbWatcherCheck(
+ async (userId: T['u']['_id'], clientAction: ClientAction = 'updated'): Promise => {
+ const items = Rooms.findDMsByUids([userId]);
+ for await (const item of items) {
+ void api.broadcast('watch.rooms', { clientAction, room: item });
+ }
+ },
+);
+
+export const notifyOnPermissionChanged = withDbWatcherCheck(
+ async (permission: IPermission, clientAction: ClientAction = 'updated'): Promise => {
+ void api.broadcast('permission.changed', { clientAction, data: permission });
+
+ if (permission.level === 'settings' && permission.settingId) {
+ const setting = await Settings.findOneNotHiddenById(permission.settingId);
+ if (!setting) {
+ return;
+ }
+ void notifyOnSettingChanged(setting, 'updated');
+ }
+ },
+);
- if (permission.level === 'settings' && permission.settingId) {
- const setting = await Settings.findOneNotHiddenById(permission.settingId);
- if (!setting) {
+export const notifyOnPermissionChangedById = withDbWatcherCheck(
+ async (pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise => {
+ const permission = await Permissions.findOneById(pid);
+ if (!permission) {
return;
}
- void notifyOnSettingChanged(setting, 'updated');
- }
-}
-export async function notifyOnPermissionChangedById(pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const permission = await Permissions.findOneById(pid);
- if (!permission) {
- return;
- }
-
- return notifyOnPermissionChanged(permission, clientAction);
-}
-
-export async function notifyOnPbxEventChangedById(
- id: T['_id'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ return notifyOnPermissionChanged(permission, clientAction);
+ },
+);
- const item = await PbxEvents.findOneById(id);
- if (!item) {
- return;
- }
-
- void api.broadcast('watch.pbxevents', { clientAction, id, data: item });
-}
-
-export async function notifyOnRoleChanged(role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('watch.roles', { clientAction, role });
-}
-
-export async function notifyOnRoleChangedById(
- id: T['_id'],
- clientAction: 'removed' | 'changed' = 'changed',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const role = await Roles.findOneById(id);
- if (!role) {
- return;
- }
-
- void notifyOnRoleChanged(role, clientAction);
-}
-
-export async function notifyOnLoginServiceConfigurationChanged(
- service: Partial & Pick,
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('watch.loginServiceConfiguration', {
- clientAction,
- id: service._id,
- data: service,
- });
-}
-
-export async function notifyOnLoginServiceConfigurationChangedByService(
- service: T['service'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const item = await LoginServiceConfiguration.findOneByService>(service, {
- projection: { secret: 0 },
- });
- if (!item) {
- return;
- }
-
- void notifyOnLoginServiceConfigurationChanged(item, clientAction);
-}
-
-export async function notifyOnIntegrationChanged(data: T, clientAction: ClientAction = 'updated'): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('watch.integrations', { clientAction, id: data._id, data });
-}
+export const notifyOnPbxEventChangedById = withDbWatcherCheck(
+ async (id: T['_id'], clientAction: ClientAction = 'updated'): Promise => {
+ const item = await PbxEvents.findOneById(id);
+ if (!item) {
+ return;
+ }
-export async function notifyOnIntegrationChangedById(
- id: T['_id'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ void api.broadcast('watch.pbxevents', { clientAction, id, data: item });
+ },
+);
- const item = await Integrations.findOneById(id);
- if (!item) {
- return;
- }
+export const notifyOnRoleChanged = withDbWatcherCheck(
+ async (role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise => {
+ void api.broadcast('watch.roles', { clientAction, role });
+ },
+);
- void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item });
-}
+export const notifyOnRoleChangedById = withDbWatcherCheck(
+ async (id: T['_id'], clientAction: 'removed' | 'changed' = 'changed'): Promise => {
+ const role = await Roles.findOneById(id);
+ if (!role) {
+ return;
+ }
-export async function notifyOnIntegrationChangedByUserId(
- id: T['userId'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ void notifyOnRoleChanged(role, clientAction);
+ },
+);
+
+export const notifyOnLoginServiceConfigurationChanged = withDbWatcherCheck(
+ async (
+ service: Partial & Pick,
+ clientAction: ClientAction = 'updated',
+ ): Promise => {
+ void api.broadcast('watch.loginServiceConfiguration', {
+ clientAction,
+ id: service._id,
+ data: service,
+ });
+ },
+);
+
+export const notifyOnLoginServiceConfigurationChangedByService = withDbWatcherCheck(
+ async (service: T['service'], clientAction: ClientAction = 'updated'): Promise => {
+ const item = await LoginServiceConfiguration.findOneByService>(service, {
+ projection: { secret: 0 },
+ });
+ if (!item) {
+ return;
+ }
- const items = Integrations.findByUserId(id);
+ void notifyOnLoginServiceConfigurationChanged(item, clientAction);
+ },
+);
- for await (const item of items) {
- void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item });
- }
-}
+export const notifyOnIntegrationChanged = withDbWatcherCheck(
+ async (data: T, clientAction: ClientAction = 'updated'): Promise => {
+ void api.broadcast('watch.integrations', { clientAction, id: data._id, data });
+ },
+);
-export async function notifyOnIntegrationChangedByChannels(
- channels: T['channel'],
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const items = Integrations.findByChannels(channels);
+export const notifyOnIntegrationChangedById = withDbWatcherCheck(
+ async (id: T['_id'], clientAction: ClientAction = 'updated'): Promise => {
+ const item = await Integrations.findOneById(id);
+ if (!item) {
+ return;
+ }
- for await (const item of items) {
void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item });
- }
-}
-
-export async function notifyOnEmailInboxChanged(
- data: Pick | T, // TODO: improve typing
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data });
-}
-
-export async function notifyOnLivechatInquiryChanged(
- data: ILivechatInquiryRecord | ILivechatInquiryRecord[],
- clientAction: ClientAction = 'updated',
- diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const items = Array.isArray(data) ? data : [data];
-
- for (const item of items) {
- void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff });
- }
-}
-
-export async function notifyOnLivechatInquiryChangedById(
- id: ILivechatInquiryRecord['_id'],
- clientAction: ClientAction = 'updated',
- diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const inquiry = clientAction === 'removed' ? await LivechatInquiry.trashFindOneById(id) : await LivechatInquiry.findOneById(id);
-
- if (!inquiry) {
- return;
- }
-
- void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
-}
-
-export async function notifyOnLivechatInquiryChangedByRoom(
- rid: ILivechatInquiryRecord['rid'],
- clientAction: ClientAction = 'updated',
- diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- const inquiry = await LivechatInquiry.findOneByRoomId(rid, {});
-
- if (!inquiry) {
- return;
- }
-
- void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
-}
-
-export async function notifyOnLivechatInquiryChangedByToken(
- token: ILivechatInquiryRecord['v']['token'],
- clientAction: ClientAction = 'updated',
- diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ },
+);
- const inquiry = await LivechatInquiry.findOneByToken(token);
-
- if (!inquiry) {
- return;
- }
-
- void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
-}
-
-export async function notifyOnIntegrationHistoryChanged(
- data: AtLeast,
- clientAction: ClientAction = 'updated',
- diff: Partial = {},
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
-
- void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff });
-}
-
-export async function notifyOnIntegrationHistoryChangedById(
- id: T['_id'],
- clientAction: ClientAction = 'updated',
- diff: Partial = {},
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+export const notifyOnIntegrationChangedByUserId = withDbWatcherCheck(
+ async (id: T['userId'], clientAction: ClientAction = 'updated'): Promise => {
+ const items = Integrations.findByUserId(id);
- const item = await IntegrationHistory.findOneById(id);
+ for await (const item of items) {
+ void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item });
+ }
+ },
+);
- if (!item) {
- return;
- }
+export const notifyOnIntegrationChangedByChannels = withDbWatcherCheck(
+ async (channels: T['channel'], clientAction: ClientAction = 'updated'): Promise => {
+ const items = Integrations.findByChannels(channels);
- void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff });
-}
+ for await (const item of items) {
+ void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item });
+ }
+ },
+);
+
+export const notifyOnEmailInboxChanged = withDbWatcherCheck(
+ async (
+ data: Pick | T, // TODO: improve typing
+ clientAction: ClientAction = 'updated',
+ ): Promise => {
+ void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data });
+ },
+);
+
+export const notifyOnLivechatInquiryChanged = withDbWatcherCheck(
+ async (
+ data: ILivechatInquiryRecord | ILivechatInquiryRecord[],
+ clientAction: ClientAction = 'updated',
+ diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
+ ): Promise => {
+ const items = Array.isArray(data) ? data : [data];
+
+ for (const item of items) {
+ void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff });
+ }
+ },
+);
+
+export const notifyOnLivechatInquiryChangedById = withDbWatcherCheck(
+ async (
+ id: ILivechatInquiryRecord['_id'],
+ clientAction: ClientAction = 'updated',
+ diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
+ ): Promise => {
+ const inquiry = clientAction === 'removed' ? await LivechatInquiry.trashFindOneById(id) : await LivechatInquiry.findOneById(id);
+
+ if (!inquiry) {
+ return;
+ }
-export async function notifyOnLivechatDepartmentAgentChanged(
- data: Partial & Pick,
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
+ },
+);
- void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data });
-}
+export const notifyOnLivechatInquiryChangedByRoom = withDbWatcherCheck(
+ async (
+ rid: ILivechatInquiryRecord['rid'],
+ clientAction: ClientAction = 'updated',
+ diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
+ ): Promise => {
+ const inquiry = await LivechatInquiry.findOneByRoomId(rid, {});
-export async function notifyOnLivechatDepartmentAgentChangedByDepartmentId(
- departmentId: T['departmentId'],
- clientAction: 'inserted' | 'updated' = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ if (!inquiry) {
+ return;
+ }
- const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } });
+ void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
+ },
+);
- for await (const item of items) {
- void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item });
- }
-}
+export const notifyOnLivechatInquiryChangedByToken = withDbWatcherCheck(
+ async (
+ token: ILivechatInquiryRecord['v']['token'],
+ clientAction: ClientAction = 'updated',
+ diff?: Partial & { queuedAt: unknown; takenAt: unknown }>,
+ ): Promise => {
+ const inquiry = await LivechatInquiry.findOneByToken(token);
-export async function notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId(
- agentsIds: T['agentId'][],
- departmentId: T['departmentId'],
- clientAction: 'inserted' | 'updated' = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
+ if (!inquiry) {
+ return;
+ }
- const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, {
- projection: { _id: 1, agentId: 1, departmentId: 1 },
- });
+ void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
+ },
+);
+
+export const notifyOnIntegrationHistoryChanged = withDbWatcherCheck(
+ async (
+ data: AtLeast,
+ clientAction: ClientAction = 'updated',
+ diff: Partial = {},
+ ): Promise => {
+ void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff });
+ },
+);
+
+export const notifyOnIntegrationHistoryChangedById = withDbWatcherCheck(
+ async (id: T['_id'], clientAction: ClientAction = 'updated', diff: Partial = {}): Promise => {
+ const item = await IntegrationHistory.findOneById(id);
+
+ if (!item) {
+ return;
+ }
- for await (const item of items) {
- void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item });
- }
-}
+ void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff });
+ },
+);
+
+export const notifyOnLivechatDepartmentAgentChanged = withDbWatcherCheck(
+ async (
+ data: Partial & Pick,
+ clientAction: ClientAction = 'updated',
+ ): Promise => {
+ void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data });
+ },
+);
+
+export const notifyOnLivechatDepartmentAgentChangedByDepartmentId = withDbWatcherCheck(
+ async (
+ departmentId: T['departmentId'],
+ clientAction: 'inserted' | 'updated' = 'updated',
+ ): Promise => {
+ const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } });
+
+ for await (const item of items) {
+ void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item });
+ }
+ },
+);
+
+export const notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId = withDbWatcherCheck(
+ async (
+ agentsIds: T['agentId'][],
+ departmentId: T['departmentId'],
+ clientAction: 'inserted' | 'updated' = 'updated',
+ ): Promise => {
+ const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, {
+ projection: { _id: 1, agentId: 1, departmentId: 1 },
+ });
+
+ for await (const item of items) {
+ void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item });
+ }
+ },
+);
-export async function notifyOnSettingChanged(
- setting: ISetting & { editor?: ISettingColor['editor'] },
- clientAction: ClientAction = 'updated',
-): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
- void api.broadcast('watch.settings', { clientAction, setting });
-}
+export const notifyOnSettingChanged = withDbWatcherCheck(
+ async (setting: ISetting & { editor?: ISettingColor['editor'] }, clientAction: ClientAction = 'updated'): Promise => {
+ void api.broadcast('watch.settings', { clientAction, setting });
+ },
+);
-export async function notifyOnSettingChangedById(id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise {
- if (!dbWatchersDisabled) {
- return;
- }
- const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id);
+export const notifyOnSettingChangedById = withDbWatcherCheck(
+ async (id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise => {
+ const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id);
- if (!item) {
- return;
- }
+ if (!item) {
+ return;
+ }
- void api.broadcast('watch.settings', { clientAction, setting: item });
-}
+ void api.broadcast('watch.settings', { clientAction, setting: item });
+ },
+);
type NotifyUserChange = {
id: IUser['_id'];
@@ -452,30 +359,24 @@ type NotifyUserChange = {
unset?: Record;
};
-export async function notifyOnUserChange({ clientAction, id, data, diff, unset }: NotifyUserChange) {
- if (!dbWatchersDisabled) {
- return;
- }
+export const notifyOnUserChange = withDbWatcherCheck(async ({ clientAction, id, data, diff, unset }: NotifyUserChange) => {
if (clientAction === 'removed') {
void api.broadcast('watch.users', { clientAction, id });
return;
}
+
if (clientAction === 'inserted') {
void api.broadcast('watch.users', { clientAction, id, data: data! });
return;
}
void api.broadcast('watch.users', { clientAction, diff: diff!, unset: unset || {}, id });
-}
+});
/**
* Calls the callback only if DB Watchers are disabled
*/
-export async function notifyOnUserChangeAsync(cb: () => Promise) {
- if (!dbWatchersDisabled) {
- return;
- }
-
+export const notifyOnUserChangeAsync = withDbWatcherCheck(async (cb: () => Promise) => {
const result = await cb();
if (!result) {
return;
@@ -487,17 +388,16 @@ export async function notifyOnUserChangeAsync(cb: () => Promise {
+ const user = await Users.findOneById(id);
+ if (!user) {
+ return;
+ }
- void notifyOnUserChange({ id, clientAction, data: user });
-}
+ void notifyOnUserChange({ id, clientAction, data: user });
+ },
+);
diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
index 44654428ae8f..49fcc0ea4725 100644
--- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
+++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
@@ -266,7 +266,7 @@ export async function sendMessageNotifications(message: IMessage, room: IRoom, u
return;
}
- const sender = await roomCoordinator.getRoomDirectives(room.t).getMsgSender(message.u._id);
+ const sender = await roomCoordinator.getRoomDirectives(room.t).getMsgSender(message);
if (!sender) {
return message;
}
diff --git a/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx
index c70f32c11c0d..41af8a22b6bb 100644
--- a/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx
+++ b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx
@@ -1,6 +1,5 @@
import { Button, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
-import type { FC } from 'react';
import React from 'react';
type PlaceChatOnHoldModalProps = {
@@ -9,7 +8,7 @@ type PlaceChatOnHoldModalProps = {
onCancel: () => void;
};
-const PlaceChatOnHoldModal: FC = ({ onCancel, onOnHoldChat, confirm = onOnHoldChat, ...props }) => {
+const PlaceChatOnHoldModal = ({ onCancel, onOnHoldChat, confirm = onOnHoldChat, ...props }: PlaceChatOnHoldModalProps) => {
const t = useTranslation();
return (
diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts
index f7d5ddb314c9..f80ed61a131e 100644
--- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts
@@ -30,7 +30,7 @@ API.v1.addRoute(
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields } = await this.parseJsonQuery();
- const { agents, departmentId, open, tags, roomName, onhold } = this.queryParams;
+ const { agents, departmentId, open, tags, roomName, onhold, queued } = this.queryParams;
const { createdAt, customFields, closedAt } = this.queryParams;
const createdAtParam = validateDateParams('createdAt', createdAt);
@@ -69,6 +69,7 @@ API.v1.addRoute(
tags,
customFields: parsedCf,
onhold,
+ queued,
options: { offset, count, sort, fields },
}),
);
diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts
index f6502b70f68a..6f8ce64bc635 100644
--- a/apps/meteor/app/livechat/imports/server/rest/sms.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts
@@ -73,8 +73,13 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
data.department = targetDepartment;
}
- const id = await LivechatTyped.registerGuest(data);
- return LivechatVisitors.findOneEnabledById(id);
+ const livechatVisitor = await LivechatTyped.registerGuest(data);
+
+ if (!livechatVisitor) {
+ throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor');
+ }
+
+ return livechatVisitor;
};
const normalizeLocationSharing = (payload: ServiceData) => {
@@ -110,12 +115,6 @@ API.v1.addRoute('livechat/sms-incoming/:service', {
return API.v1.success(SMSService.error(new Error('Invalid visitor')));
}
- const { token } = visitor;
- const room = await LivechatRooms.findOneOpenByVisitorTokenAndDepartmentIdAndSource(token, targetDepartment, OmnichannelSourceType.SMS);
- const roomExists = !!room;
- const location = normalizeLocationSharing(sms);
- const rid = room?._id || Random.id();
-
const roomInfo = {
sms: {
from: sms.to,
@@ -126,10 +125,15 @@ API.v1.addRoute('livechat/sms-incoming/:service', {
},
};
- // create an empty room first place, so attachments have a place to live
- if (!roomExists) {
- await LivechatTyped.getRoom(visitor, { rid, token, msg: '' }, roomInfo, undefined);
- }
+ const { token } = visitor;
+ const room =
+ (await LivechatRooms.findOneOpenByVisitorTokenAndDepartmentIdAndSource(token, targetDepartment, OmnichannelSourceType.SMS)) ??
+ (await LivechatTyped.createRoom({
+ visitor,
+ roomInfo,
+ }));
+ const location = normalizeLocationSharing(sms);
+ const rid = room?._id;
let file: ILivechatMessage['file'];
const attachments: (MessageAttachment | undefined)[] = [];
diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts
index 00229dae2de5..617d255cb6cb 100644
--- a/apps/meteor/app/livechat/server/api/lib/livechat.ts
+++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts
@@ -1,14 +1,6 @@
-import type {
- ILivechatAgent,
- ILivechatDepartment,
- ILivechatTrigger,
- ILivechatVisitor,
- IOmnichannelRoom,
- SelectedAgent,
-} from '@rocket.chat/core-typings';
+import type { ILivechatAgent, ILivechatDepartment, ILivechatTrigger, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
-import { Random } from '@rocket.chat/random';
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../../lib/callbacks';
@@ -104,33 +96,6 @@ export async function findOpenRoom(token: string, departmentId?: string): Promis
return rooms[0];
}
}
-export function getRoom({
- guest,
- rid,
- roomInfo,
- agent,
- extraParams,
-}: {
- guest: ILivechatVisitor;
- rid: string;
- roomInfo: {
- source?: IOmnichannelRoom['source'];
- };
- agent?: SelectedAgent;
- extraParams?: Record;
-}): Promise<{ room: IOmnichannelRoom; newRoom: boolean }> {
- const token = guest?.token;
-
- const message = {
- _id: Random.id(),
- rid,
- msg: '',
- token,
- ts: new Date(),
- };
-
- return LivechatTyped.getRoom(guest, message, roomInfo, agent, extraParams);
-}
export async function findAgent(agentId?: string): Promise {
return normalizeAgent(agentId);
diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts
index b130e5c2c73a..26449dce3963 100644
--- a/apps/meteor/app/livechat/server/api/lib/rooms.ts
+++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts
@@ -14,6 +14,7 @@ export async function findRooms({
tags,
customFields,
onhold,
+ queued,
options: { offset, count, fields, sort },
}: {
agents?: Array;
@@ -31,6 +32,7 @@ export async function findRooms({
tags?: Array;
customFields?: Record;
onhold?: string | boolean;
+ queued?: string | boolean;
options: { offset: number; count: number; fields: Record; sort: Record };
}): Promise }>> {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
@@ -44,6 +46,7 @@ export async function findRooms({
tags,
customFields,
onhold: ['t', 'true', '1'].includes(`${onhold}`),
+ queued: ['t', 'true', '1'].includes(`${queued}`),
options: {
sort: sort || { ts: -1 },
offset,
diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts
index 97c92eeb530f..b7eb6e1f684a 100644
--- a/apps/meteor/app/livechat/server/api/v1/message.ts
+++ b/apps/meteor/app/livechat/server/api/v1/message.ts
@@ -251,7 +251,7 @@ API.v1.addRoute(
async post() {
const visitorToken = this.bodyParams.visitor.token;
- let visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {});
+ const visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {});
let rid: string;
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
@@ -267,8 +267,10 @@ API.v1.addRoute(
const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor;
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
- const visitorId = await LivechatTyped.registerGuest(guest);
- visitor = await LivechatVisitors.findOneEnabledById(visitorId);
+ const visitor = await LivechatTyped.registerGuest(guest);
+ if (!visitor) {
+ throw new Error('error-livechat-visitor-registration');
+ }
}
const guest = visitor;
diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts
index d2a76e53926f..b0f45a63ff87 100644
--- a/apps/meteor/app/livechat/server/api/v1/room.ts
+++ b/apps/meteor/app/livechat/server/api/v1/room.ts
@@ -1,8 +1,7 @@
import { Omnichannel } from '@rocket.chat/core-services';
-import type { ILivechatAgent, IOmnichannelRoom, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings';
+import type { ILivechatAgent, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models';
-import { Random } from '@rocket.chat/random';
import {
isLiveChatRoomForwardProps,
isPOSTLivechatRoomCloseParams,
@@ -27,7 +26,7 @@ import { settings as rcSettings } from '../../../../settings/server';
import { normalizeTransferredByData } from '../../lib/Helper';
import type { CloseRoomParams } from '../../lib/LivechatTyped';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
-import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat';
+import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat';
import { findVisitorInfo } from '../lib/visitors';
const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj);
@@ -43,16 +42,15 @@ API.v1.addRoute('livechat/room', {
check(this.queryParams, extraCheckParams as any);
- const { token, rid: roomId, agentId, ...extraParams } = this.queryParams;
+ const { token, rid, agentId, ...extraParams } = this.queryParams;
const guest = token && (await findGuest(token));
if (!guest) {
throw new Error('invalid-token');
}
- let room: IOmnichannelRoom | null;
- if (!roomId) {
- room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
+ if (!rid) {
+ const room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
if (room) {
return API.v1.success({ room, newRoom: false });
}
@@ -68,18 +66,21 @@ API.v1.addRoute('livechat/room', {
}
}
- const rid = Random.id();
const roomInfo = {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
},
};
- const newRoom = await getRoom({ guest, rid, agent, roomInfo, extraParams });
- return API.v1.success(newRoom);
+ const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams });
+
+ return API.v1.success({
+ room: newRoom,
+ newRoom: true,
+ });
}
- const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token, {});
+ const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {});
if (!froom) {
throw new Error('invalid-room');
}
@@ -292,8 +293,7 @@ API.v1.addRoute(
throw new Error('error-invalid-visitor');
}
- const transferedBy = this.user satisfies TransferByData;
- transferData.transferredBy = normalizeTransferredByData(transferedBy, room);
+ transferData.transferredBy = normalizeTransferredByData(this.user, room);
if (transferData.userId) {
const userToTransfer = await Users.findOneById(transferData.userId);
if (userToTransfer) {
diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts
index 9c19f5bbdec8..a5b3f2de35b1 100644
--- a/apps/meteor/app/livechat/server/api/v1/visitor.ts
+++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts
@@ -1,4 +1,4 @@
-import type { ILivechatCustomField, ILivechatVisitor, IRoom } from '@rocket.chat/core-typings';
+import type { ILivechatCustomField, IRoom } from '@rocket.chat/core-typings';
import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -47,27 +47,29 @@ API.v1.addRoute('livechat/visitor', {
connectionData: normalizeHttpHeaderData(this.request.headers),
};
- const visitorId = await LivechatTyped.registerGuest(guest);
-
- let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(visitorId, {});
- if (visitor) {
- const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
- // If it's updating an existing visitor, it must also update the roomInfo
- const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
- await Promise.all(
- rooms.map(
- (room: IRoom) =>
- visitor &&
- LivechatTyped.saveRoomInfo(room, {
- _id: visitor._id,
- name: visitor.name,
- phone: visitor.phone?.[0]?.phoneNumber,
- livechatData: visitor.livechatData as { [k: string]: string },
- }),
- ),
- );
+ const visitor = await LivechatTyped.registerGuest(guest);
+ if (!visitor) {
+ throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
+ method: 'livechat/visitor',
+ });
}
+ const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
+ // If it's updating an existing visitor, it must also update the roomInfo
+ const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
+ await Promise.all(
+ rooms.map(
+ (room: IRoom) =>
+ visitor &&
+ LivechatTyped.saveRoomInfo(room, {
+ _id: visitor._id,
+ name: visitor.name,
+ phone: visitor.phone?.[0]?.phoneNumber,
+ livechatData: visitor.livechatData as { [k: string]: string },
+ }),
+ ),
+ );
+
if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const keys = customFields.map((field) => field.key);
const errors: string[] = [];
@@ -96,7 +98,7 @@ API.v1.addRoute('livechat/visitor', {
if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
- visitorId,
+ visitorId: visitor._id,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
}
@@ -104,13 +106,13 @@ API.v1.addRoute('livechat/visitor', {
if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
- visitorId,
+ visitorId: visitor._id,
errors,
});
throw new Error('error-updating-custom-fields');
}
- visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
+ return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) });
}
if (!visitor) {
diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts
index 5c3a2c0b54ab..24e1d685a0e6 100644
--- a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts
+++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts
@@ -180,18 +180,11 @@ callbacks.add(
callbacks.add(
'livechat.afterTakeInquiry',
- async (inquiry) => {
+ async ({ inquiry, room }) => {
if (!settings.get('Livechat_webhook_on_chat_taken')) {
return inquiry;
}
- const { rid } = inquiry;
- const room = await LivechatRooms.findOneById(rid);
-
- if (!room) {
- return inquiry;
- }
-
return sendToCRM('LivechatSessionTaken', room);
},
callbacks.priority.MEDIUM,
diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts
index dacd99be00f9..c0e85a8c7c2b 100644
--- a/apps/meteor/app/livechat/server/lib/Helper.ts
+++ b/apps/meteor/app/livechat/server/lib/Helper.ts
@@ -4,7 +4,6 @@ import { api, Message, Omnichannel } from '@rocket.chat/core-services';
import type {
ILivechatVisitor,
IOmnichannelRoom,
- IMessage,
SelectedAgent,
ISubscription,
ILivechatInquiryRecord,
@@ -30,6 +29,7 @@ import {
} from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
+import { ObjectId } from 'mongodb';
import { callbacks } from '../../../../lib/callbacks';
import { validateEmail as validatorFunc } from '../../../../lib/emailValidator';
@@ -57,12 +57,18 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => {
return hasRoleAsync(agent.agentId, 'bot');
};
-export const createLivechatRoom = async (
+export const createLivechatRoom = async <
+ E extends Record & {
+ sla?: string;
+ customFields?: Record;
+ source?: OmnichannelSourceType;
+ },
+>(
rid: string,
name: string,
guest: ILivechatVisitor,
roomInfo: Partial = {},
- extraData = {},
+ extraData?: E,
) => {
check(rid, String);
check(name, String);
@@ -86,47 +92,61 @@ export const createLivechatRoom = async (
visitor: { _id, username, departmentId, status, activity },
});
- const room: InsertionModel = Object.assign(
- {
- _id: rid,
- msgs: 0,
- usersCount: 1,
- lm: newRoomAt,
- fname: name,
- t: 'l' as const,
- ts: newRoomAt,
- departmentId,
- v: {
- _id,
- username,
- token,
- status,
- ...(activity?.length && { activity }),
- },
- cl: false,
- open: true,
- waitingResponse: true,
- // this should be overriden by extraRoomInfo when provided
- // in case it's not provided, we'll use this "default" type
- source: {
- type: OmnichannelSourceType.OTHER,
- alias: 'unknown',
- },
- queuedAt: newRoomAt,
+ // TODO: Solve `u` missing issue
+ const room: InsertionModel = {
+ _id: rid,
+ msgs: 0,
+ usersCount: 1,
+ lm: newRoomAt,
+ fname: name,
+ t: 'l' as const,
+ ts: newRoomAt,
+ departmentId,
+ v: {
+ _id,
+ username,
+ token,
+ status,
+ ...(activity?.length && { activity }),
+ },
+ cl: false,
+ open: true,
+ waitingResponse: true,
+ // this should be overridden by extraRoomInfo when provided
+ // in case it's not provided, we'll use this "default" type
+ source: {
+ type: OmnichannelSourceType.OTHER,
+ alias: 'unknown',
+ },
+ queuedAt: newRoomAt,
+ livechatData: undefined,
+ priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
+ estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
+ ...extraRoomInfo,
+ } as InsertionModel;
- priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
- estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
+ const result = await Rooms.findOneAndUpdate(
+ room,
+ {
+ $set: {},
+ },
+ {
+ upsert: true,
+ returnDocument: 'after',
},
- extraRoomInfo,
);
- const roomId = (await Rooms.insertOne(room)).insertedId;
+ if (!result.value) {
+ throw new Error('Room not created');
+ }
await callbacks.run('livechat.newRoom', room);
- await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false }, room);
+ // TODO: replace with `Message.saveSystemMessage`
+
+ await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false, token: guest.token }, room);
- return roomId;
+ return result.value as IOmnichannelRoom;
};
export const createLivechatInquiry = async ({
@@ -140,7 +160,7 @@ export const createLivechatInquiry = async ({
rid: string;
name?: string;
guest?: Pick;
- message?: Pick;
+ message?: string;
initialStatus?: LivechatInquiryStatus;
extraData?: Pick;
}) => {
@@ -156,17 +176,11 @@ export const createLivechatInquiry = async ({
activity: Match.Maybe([String]),
}),
);
- check(
- message,
- Match.ObjectIncluding({
- msg: String,
- }),
- );
const extraInquiryInfo = await callbacks.run('livechat.beforeInquiry', extraData);
const { _id, username, token, department, status = UserStatus.ONLINE, activity } = guest;
- const { msg } = message;
+
const ts = new Date();
logger.debug({
@@ -174,31 +188,44 @@ export const createLivechatInquiry = async ({
visitor: { _id, username, department, status, activity },
});
- const inquiry: InsertionModel = {
- rid,
- name,
- ts,
- department,
- message: msg,
- status: initialStatus || LivechatInquiryStatus.READY,
- v: {
- _id,
- username,
- token,
- status,
- ...(activity?.length && { activity }),
- },
- t: 'l',
- priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
- estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
-
- ...extraInquiryInfo,
- };
+ const result = await LivechatInquiry.findOneAndUpdate(
+ {
+ rid,
+ name,
+ ts,
+ department,
+ message: message ?? '',
+ status: initialStatus || LivechatInquiryStatus.READY,
+ v: {
+ _id,
+ username,
+ token,
+ status,
+ ...(activity?.length && { activity }),
+ },
+ t: 'l',
+ priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
+ estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
- const result = (await LivechatInquiry.insertOne(inquiry)).insertedId;
+ ...extraInquiryInfo,
+ },
+ {
+ $set: {
+ _id: new ObjectId().toHexString(),
+ },
+ },
+ {
+ upsert: true,
+ returnDocument: 'after',
+ },
+ );
logger.debug(`Inquiry ${result} created for visitor ${_id}`);
- return result;
+ if (!result.value) {
+ throw new Error('Inquiry not created');
+ }
+
+ return result.value as ILivechatInquiryRecord;
};
export const createLivechatSubscription = async (
@@ -337,6 +364,10 @@ export const dispatchAgentDelegated = async (rid: string, agentId?: string) => {
});
};
+/**
+ * @deprecated
+ */
+
export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, agent?: SelectedAgent | null) => {
if (!inquiry?._id) {
return;
@@ -355,10 +386,12 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age
return;
}
- if (!agent || !(await allowAgentSkipQueue(agent))) {
- await saveQueueInquiry(inquiry);
+ if (agent && (await allowAgentSkipQueue(agent))) {
+ return;
}
+ await saveQueueInquiry(inquiry);
+
// Alert only the online agents of the queued request
const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent);
if (!onlineAgents) {
@@ -439,9 +472,14 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T
// There are some Enterprise features that may interrupt the forwarding process
// Due to that we need to check whether the agent has been changed or not
logger.debug(`Forwarding inquiry ${inquiry._id} to agent ${agent.agentId}`);
- const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, {
- ...(clientAction && { clientAction }),
- });
+ const roomTaken = await RoutingManager.takeInquiry(
+ inquiry,
+ agent,
+ {
+ ...(clientAction && { clientAction }),
+ },
+ room,
+ );
if (!roomTaken) {
logger.debug(`Cannot forward inquiry ${inquiry._id}`);
return false;
@@ -566,10 +604,15 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
// Fake the department to forward the inquiry - Case the forward process does not success
// the inquiry will stay in the same original department
inquiry.department = departmentId;
- const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, {
- forwardingToDepartment: { oldDepartmentId },
- ...(clientAction && { clientAction }),
- });
+ const roomTaken = await RoutingManager.delegateInquiry(
+ inquiry,
+ agent,
+ {
+ forwardingToDepartment: { oldDepartmentId },
+ ...(clientAction && { clientAction }),
+ },
+ room,
+ );
if (!roomTaken) {
logger.debug(`Cannot forward room ${room._id}. Unable to delegate inquiry`);
return false;
@@ -605,6 +648,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
'',
{ _id, username },
{
+ ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }),
transferData: {
...transferData,
prevDepartment: transferData.originalDepartmentName,
@@ -640,31 +684,26 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
return true;
};
-export const normalizeTransferredByData = (transferredBy: TransferByData, room: IOmnichannelRoom) => {
+type MakePropertyOptional = Omit & { [P in K]?: T[P] };
+
+export const normalizeTransferredByData = (
+ transferredBy: MakePropertyOptional,
+ room: IOmnichannelRoom,
+): TransferByData => {
if (!transferredBy || !room) {
throw new Error('You must provide "transferredBy" and "room" params to "getTransferredByData"');
}
const { servedBy: { _id: agentId } = {} } = room;
const { _id, username, name, userType: transferType } = transferredBy;
- const type = transferType || (_id === agentId ? 'agent' : 'user');
+ const userType = transferType || (_id === agentId ? 'agent' : 'user');
return {
_id,
username,
...(name && { name }),
- type,
+ userType,
};
};
-export const checkServiceStatus = async ({ guest, agent }: { guest: Pick; agent?: SelectedAgent }) => {
- if (!agent) {
- return LivechatTyped.online(guest.department);
- }
-
- const { agentId } = agent;
- const users = await Users.countOnlineAgents(agentId);
- return users > 0;
-};
-
const parseFromIntOrStr = (value: string | number) => {
if (typeof value === 'number') {
return value;
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index bf5014b984f1..ccca7a8eb68e 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -9,7 +9,6 @@ import type {
IUser,
MessageTypesValues,
ILivechatVisitor,
- IOmnichannelSystemMessage,
SelectedAgent,
ILivechatAgent,
IMessage,
@@ -21,6 +20,7 @@ import type {
IOmnichannelAgent,
ILivechatDepartmentAgents,
LivechatDepartmentDTO,
+ OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger, type MainLogger } from '@rocket.chat/logger';
@@ -37,12 +37,10 @@ import {
Rooms,
LivechatCustomField,
} from '@rocket.chat/models';
-import { Random } from '@rocket.chat/random';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
-import moment from 'moment-timezone';
-import type { Filter, FindCursor, UpdateFilter } from 'mongodb';
+import type { Filter, FindCursor } from 'mongodb';
import UAParser from 'ua-parser-js';
import { callbacks } from '../../../../lib/callbacks';
@@ -68,43 +66,22 @@ import {
import * as Mailer from '../../../mailer/server/api';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
-import { getTimezone } from '../../../utils/server/lib/getTimezone';
import { businessHourManager } from '../business-hour';
import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper';
import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';
-
-type GenericCloseRoomParams = {
- room: IOmnichannelRoom;
- comment?: string;
- options?: {
- clientAction?: boolean;
- tags?: string[];
- emailTranscript?:
- | {
- sendToVisitor: false;
- }
- | {
- sendToVisitor: true;
- requestData: NonNullable;
- };
- pdfTranscript?: {
- requestedBy: string;
- };
- };
+import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes';
+import { parseTranscriptRequest } from './parseTranscriptRequest';
+import { sendTranscript as sendTranscriptFunc } from './sendTranscript';
+
+type RegisterGuestType = Partial> & {
+ id?: string;
+ connectionData?: any;
+ email?: string;
+ phone?: { number: string };
};
-export type CloseRoomParamsByUser = {
- user: IUser | null;
-} & GenericCloseRoomParams;
-
-export type CloseRoomParamsByVisitor = {
- visitor: ILivechatVisitor;
-} & GenericCloseRoomParams;
-
-export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor;
-
type OfflineMessageData = {
message: string;
name: string;
@@ -235,7 +212,7 @@ class LivechatClass {
return;
}
- return Users.findByIds(agentIds);
+ return Users.findByIds([...new Set(agentIds)]);
}
return Users.findOnlineAgents();
}
@@ -319,12 +296,8 @@ class LivechatClass {
this.logger.debug(`DB updated for room ${room._id}`);
- const message = {
- t: 'livechat-close',
- msg: comment,
- groupable: false,
- transcriptRequested: !!transcriptRequest,
- };
+ const transcriptRequested =
+ !!transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));
// Retrieve the closed room
const newRoom = await LivechatRooms.findOneById(rid);
@@ -334,9 +307,21 @@ class LivechatClass {
}
this.logger.debug(`Sending closing message to room ${room._id}`);
- await sendMessage(chatCloser, message, newRoom);
+ await sendMessage(
+ chatCloser,
+ {
+ t: 'livechat-close',
+ msg: comment,
+ groupable: false,
+ transcriptRequested,
+ ...(isRoomClosedByVisitorParams(params) && { token: chatCloser.token }),
+ },
+ newRoom,
+ );
- await Message.saveSystemMessage('command', rid, 'promptTranscript', closeData.closedBy);
+ 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}`);
@@ -348,15 +333,18 @@ class LivechatClass {
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,
+ options: opts,
});
} else {
callbacks.runAsync('livechat.closeRoom', {
room: newRoom,
- options,
+ options: opts,
});
}
@@ -383,7 +371,66 @@ class LivechatClass {
}
}
- async getRoom(
+ async createRoom({
+ visitor,
+ message,
+ rid,
+ roomInfo,
+ agent,
+ extraData,
+ }: {
+ visitor: ILivechatVisitor;
+ message?: string;
+ rid?: string;
+ roomInfo: {
+ source?: IOmnichannelRoom['source'];
+ [key: string]: unknown;
+ };
+ agent?: SelectedAgent;
+ extraData?: Record;
+ }) {
+ if (!this.enabled()) {
+ throw new Meteor.Error('error-omnichannel-is-disabled');
+ }
+
+ const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, visitor);
+ // if no department selected verify if there is at least one active and pick the first
+ if (!defaultAgent && !visitor.department) {
+ const department = await this.getRequiredDepartment();
+ Livechat.logger.debug(`No department or default agent selected for ${visitor._id}`);
+
+ if (department) {
+ Livechat.logger.debug(`Assigning ${visitor._id} to department ${department._id}`);
+ visitor.department = department._id;
+ }
+ }
+
+ // delegate room creation to QueueManager
+ Livechat.logger.debug(`Calling QueueManager to request a room for visitor ${visitor._id}`);
+
+ const room = await QueueManager.requestRoom({
+ guest: visitor,
+ message,
+ rid,
+ roomInfo,
+ agent: defaultAgent,
+ extraData,
+ });
+
+ Livechat.logger.debug(`Room obtained for visitor ${visitor._id} -> ${room._id}`);
+
+ await Messages.setRoomIdByToken(visitor.token, room._id);
+
+ return room;
+ }
+
+ async getRoom<
+ E extends Record & {
+ sla?: string;
+ customFields?: Record;
+ source?: OmnichannelSourceType;
+ },
+ >(
guest: ILivechatVisitor,
message: Pick,
roomInfo: {
@@ -391,69 +438,31 @@ class LivechatClass {
[key: string]: unknown;
},
agent?: SelectedAgent,
- extraData?: Record,
+ extraData?: E,
) {
if (!this.enabled()) {
throw new Meteor.Error('error-omnichannel-is-disabled');
}
Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`);
- let room = await LivechatRooms.findOneById(message.rid);
- let newRoom = false;
+ const room = await LivechatRooms.findOneById(message.rid);
if (room && !room.open) {
Livechat.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`);
- message.rid = Random.id();
- room = null;
- }
-
- if (
- guest.department &&
- !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } }))
- ) {
- await LivechatVisitors.removeDepartmentById(guest._id);
- const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id);
- if (tmpGuest) {
- guest = tmpGuest;
- }
}
- if (room == null) {
- const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest);
- // if no department selected verify if there is at least one active and pick the first
- if (!defaultAgent && !guest.department) {
- const department = await this.getRequiredDepartment();
- Livechat.logger.debug(`No department or default agent selected for ${guest._id}`);
-
- if (department) {
- Livechat.logger.debug(`Assigning ${guest._id} to department ${department._id}`);
- guest.department = department._id;
- }
- }
-
- // delegate room creation to QueueManager
- Livechat.logger.debug(`Calling QueueManager to request a room for visitor ${guest._id}`);
- room = await QueueManager.requestRoom({
- guest,
- message,
- roomInfo,
- agent: defaultAgent,
- extraData,
- });
- newRoom = true;
-
- Livechat.logger.debug(`Room obtained for visitor ${guest._id} -> ${room._id}`);
+ if (!room?.open) {
+ return {
+ room: await this.createRoom({ visitor: guest, message: message.msg, roomInfo, agent, extraData }),
+ newRoom: true,
+ };
}
- if (!room || room.v.token !== guest.token) {
+ if (room.v.token !== guest.token) {
Livechat.logger.debug(`Visitor ${guest._id} trying to access another visitor's room`);
throw new Meteor.Error('cannot-access-room');
}
- if (newRoom) {
- await Messages.setRoomIdByToken(guest.token, room._id);
- }
-
- return { room, newRoom };
+ return { room, newRoom: false };
}
async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise {
@@ -534,230 +543,93 @@ class LivechatClass {
}
}
- async sendTranscript({
- token,
- rid,
- email,
- subject,
- user,
- }: {
- token: string;
- rid: string;
- email: string;
- subject?: string;
- user?: Pick | null;
- }): Promise {
- check(rid, String);
- check(email, String);
- this.logger.debug(`Sending conversation transcript of room ${rid} to user with token ${token}`);
-
- const room = await LivechatRooms.findOneById(rid);
-
- const visitor = await LivechatVisitors.getVisitorByToken(token, {
- projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
- });
-
- if (!visitor) {
- throw new Error('error-invalid-token');
- }
-
- // @ts-expect-error - Visitor typings should include language?
- const userLanguage = visitor?.language || settings.get('Language') || 'en';
- const timezone = getTimezone(user);
- this.logger.debug(`Transcript will be sent using ${timezone} as timezone`);
-
- if (!room) {
- throw new Error('error-invalid-room');
- }
-
- // allow to only user to send transcripts from their own chats
- if (room.t !== 'l' || !room.v || room.v.token !== token) {
- throw new Error('error-invalid-room');
- }
-
- const showAgentInfo = settings.get('Livechat_show_agent_info');
- const closingMessage = await Messages.findLivechatClosingMessage(rid, { projection: { ts: 1 } });
- const ignoredMessageTypes: MessageTypesValues[] = [
- 'livechat_navigation_history',
- 'livechat_transcript_history',
- 'command',
- 'livechat-close',
- 'livechat-started',
- 'livechat_video_call',
- ];
- const messages = await Messages.findVisibleByRoomIdNotContainingTypesBeforeTs(
- rid,
- ignoredMessageTypes,
- closingMessage?.ts ? new Date(closingMessage.ts) : new Date(),
- {
- sort: { ts: 1 },
- },
- );
-
- let html = '
';
- await messages.forEach((message) => {
- let author;
- if (message.u._id === visitor._id) {
- author = i18n.t('You', { lng: userLanguage });
- } else {
- author = showAgentInfo ? message.u.name || message.u.username : i18n.t('Agent', { lng: userLanguage });
- }
-
- const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL');
- const singleMessage = `
-
${author} ${datetime}
-
${message.msg}
- `;
- html += singleMessage;
- });
-
- html = `${html}
`;
-
- const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);
- let emailFromRegexp = '';
- if (fromEmail) {
- emailFromRegexp = fromEmail[0];
- } else {
- emailFromRegexp = settings.get('From_Email');
- }
-
- const mailSubject = subject || i18n.t('Transcript_of_your_livechat_conversation', { lng: userLanguage });
-
- await this.sendEmail(emailFromRegexp, email, emailFromRegexp, mailSubject, html);
-
- setImmediate(() => {
- void callbacks.run('livechat.sendTranscript', messages, email);
- });
-
- const requestData: IOmnichannelSystemMessage['requestData'] = {
- type: 'user',
- visitor,
- user,
- };
-
- if (!user?.username) {
- const cat = await Users.findOneById('rocket.cat', { projection: { _id: 1, username: 1, name: 1 } });
- if (cat) {
- requestData.user = cat;
- requestData.type = 'visitor';
- }
- }
-
- if (!requestData.user) {
- this.logger.error('rocket.cat user not found');
- throw new Error('No user provided and rocket.cat not found');
- }
-
- await Message.saveSystemMessage('livechat_transcript_history', room._id, '', requestData.user, {
- requestData,
- });
-
- return true;
- }
-
async registerGuest({
id,
token,
name,
+ phone,
email,
department,
- phone,
username,
connectionData,
status = UserStatus.ONLINE,
- }: {
- id?: string;
- token: string;
- name?: string;
- email?: string;
- department?: string;
- phone?: { number: string };
- username?: string;
- connectionData?: any;
- status?: ILivechatVisitor['status'];
- }) {
+ }: RegisterGuestType): Promise {
check(token, String);
check(id, Match.Maybe(String));
Livechat.logger.debug(`New incoming conversation: id: ${id} | token: ${token}`);
- let userId;
- type Mutable = {
- -readonly [Key in keyof Type]: Type[Key];
- };
-
- type UpdateUserType = Required, '$set'>>;
- const updateUser: Required, '$set'>> = {
- $set: {
- token,
- status,
- ...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}),
- ...(name ? { name } : {}),
- },
+ const visitorDataToUpdate: Partial & { userAgent?: string; ip?: string; host?: string } = {
+ token,
+ status,
+ ...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}),
+ ...(name ? { name } : {}),
};
if (email) {
- email = email.trim().toLowerCase();
- validateEmail(email);
- (updateUser.$set as Mutable).visitorEmails = [{ address: email }];
+ const visitorEmail = email.trim().toLowerCase();
+ validateEmail(visitorEmail);
+ visitorDataToUpdate.visitorEmails = [{ address: visitorEmail }];
}
- if (department) {
+ const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });
+
+ if (livechatVisitor?.department !== department && department) {
Livechat.logger.debug(`Attempt to find a department with id/name ${department}`);
const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } });
if (!dep) {
- Livechat.logger.debug('Invalid department provided');
+ Livechat.logger.debug(`Invalid department provided: ${department}`);
throw new Meteor.Error('error-invalid-department', 'The provided department is invalid');
}
Livechat.logger.debug(`Assigning visitor ${token} to department ${dep._id}`);
- (updateUser.$set as Mutable).department = dep._id;
+ visitorDataToUpdate.department = dep._id;
}
- const user = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });
+ visitorDataToUpdate.token = livechatVisitor?.token || token;
+
let existingUser = null;
- if (user) {
+ if (livechatVisitor) {
Livechat.logger.debug('Found matching user by token');
- userId = user._id;
+ visitorDataToUpdate._id = livechatVisitor._id;
} else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) {
Livechat.logger.debug('Found matching user by phone number');
- userId = existingUser._id;
+ visitorDataToUpdate._id = existingUser._id;
// Don't change token when matching by phone number, use current visitor token
- (updateUser.$set as Mutable).token = existingUser.token;
+ visitorDataToUpdate.token = existingUser.token;
} else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) {
Livechat.logger.debug('Found matching user by email');
- userId = existingUser._id;
- } else {
+ visitorDataToUpdate._id = existingUser._id;
+ } else if (!livechatVisitor) {
Livechat.logger.debug(`No matches found. Attempting to create new user with token ${token}`);
- if (!username) {
- username = await LivechatVisitors.getNextVisitorUsername();
- }
- const userData = {
- username,
- status,
- ts: new Date(),
- token,
- ...(id && { _id: id }),
- };
+ visitorDataToUpdate._id = id || undefined;
+ visitorDataToUpdate.username = username || (await LivechatVisitors.getNextVisitorUsername());
+ visitorDataToUpdate.status = status;
+ visitorDataToUpdate.ts = new Date();
if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) {
Livechat.logger.debug(`Saving connection data for visitor ${token}`);
- const connection = connectionData;
- if (connection?.httpHeaders) {
- (updateUser.$set as Mutable).userAgent = connection.httpHeaders['user-agent'];
- (updateUser.$set as Mutable).ip =
- connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress;
- (updateUser.$set as Mutable).host = connection.httpHeaders.host;
+ const { httpHeaders, clientAddress } = connectionData;
+ if (httpHeaders) {
+ visitorDataToUpdate.userAgent = httpHeaders['user-agent'];
+ visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress;
+ visitorDataToUpdate.host = httpHeaders?.host;
}
}
-
- userId = (await LivechatVisitors.insertOne(userData)).insertedId;
}
- await LivechatVisitors.updateById(userId, updateUser);
+ const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, {
+ upsert: true,
+ returnDocument: 'after',
+ });
+
+ if (!upsertedLivechatVisitor.value) {
+ Livechat.logger.debug(`No visitor found after upsert`);
+ return null;
+ }
- return userId;
+ return upsertedLivechatVisitor.value;
}
private async getBotAgents(department?: string) {
@@ -1255,7 +1127,7 @@ class LivechatClass {
if (guest.name) {
message.alias = guest.name;
}
- return Object.assign(await sendMessage(guest, message, room), {
+ return Object.assign(await sendMessage(guest, { ...message, token: guest.token }, room), {
newRoom,
showConnecting: this.showConnecting(),
});
@@ -1374,7 +1246,7 @@ class LivechatClass {
_id: String,
username: String,
name: Match.Maybe(String),
- type: String,
+ userType: String,
}),
);
@@ -1382,34 +1254,31 @@ class LivechatClass {
const scopeData = scope || (nextDepartment ? 'department' : 'agent');
this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`);
- const transfer = {
- transferData: {
- transferredBy,
+ await sendMessage(
+ transferredBy,
+ {
+ t: 'livechat_transfer_history',
+ rid: room._id,
ts: new Date(),
- scope: scopeData,
- comment,
- ...(previousDepartment && { previousDepartment }),
- ...(nextDepartment && { nextDepartment }),
- ...(transferredTo && { transferredTo }),
- },
- };
-
- const type = 'livechat_transfer_history';
- const transferMessage = {
- t: type,
- rid: room._id,
- ts: new Date(),
- msg: '',
- u: {
- _id,
- username,
+ 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 }),
+ },
},
- groupable: false,
- };
-
- Object.assign(transferMessage, transfer);
-
- await sendMessage(transferredBy, transferMessage, room);
+ room,
+ );
}
async saveGuest(guestData: Pick & { email?: string; phone?: string }, userId: string) {
@@ -1972,6 +1841,23 @@ 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();
+export * from './localTypes';
diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts
index 576b29990b33..e1ea79d84163 100644
--- a/apps/meteor/app/livechat/server/lib/QueueManager.ts
+++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts
@@ -1,26 +1,34 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { Omnichannel } from '@rocket.chat/core-services';
+import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import {
LivechatInquiryStatus,
type ILivechatInquiryRecord,
type ILivechatVisitor,
- type IMessage,
type IOmnichannelRoom,
type SelectedAgent,
+ type OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
-import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
+import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
+import { Random } from '@rocket.chat/random';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
+import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper';
import { callbacks } from '../../../../lib/callbacks';
+import { sendNotification } from '../../../lib/server';
import {
notifyOnLivechatInquiryChangedById,
notifyOnLivechatInquiryChanged,
notifyOnSettingChanged,
} from '../../../lib/server/lib/notifyListener';
-import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper';
+import { settings } from '../../../settings/server';
+import { i18n } from '../../../utils/lib/i18n';
+import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper';
+import { Livechat } from './LivechatTyped';
import { RoutingManager } from './RoutingManager';
+import { getInquirySortMechanismSetting } from './settings';
const logger = new Logger('QueueManager');
@@ -39,54 +47,129 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => {
});
};
+/**
+ * @deprecated
+ */
export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => {
- const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
- logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);
-
- await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
const room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } });
- if (!room || !(await Omnichannel.isWithinMACLimit(room))) {
- logger.error({ msg: 'MAC limit reached, not routing inquiry', inquiry });
- // We'll queue these inquiries so when new license is applied, they just start rolling again
- // Minimizing disruption
+
+ if (!room) {
await saveQueueInquiry(inquiry);
return;
}
- const dbInquiry = await LivechatInquiry.findOneById(inquiry._id);
- if (!dbInquiry) {
- throw new Error('inquiry-not-found');
+ return QueueManager.requeueInquiry(inquiry, room, defaultAgent);
+};
+
+const getDepartment = async (department: string): Promise => {
+ if (!department) {
+ return;
}
- if (dbInquiry.status === 'ready') {
- logger.debug(`Inquiry with id ${inquiry._id} is ready. Delegating to agent ${inquiryAgent?.username}`);
- return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent);
+ if (await LivechatDepartmentAgents.checkOnlineForDepartment(department)) {
+ return department;
+ }
+
+ const departmentDocument = await LivechatDepartment.findOneById>(
+ department,
+ {
+ projection: { fallbackForwardDepartment: 1 },
+ },
+ );
+
+ if (departmentDocument?.fallbackForwardDepartment) {
+ return getDepartment(departmentDocument.fallbackForwardDepartment);
}
};
-type queueManager = {
- requestRoom: (params: {
+export class QueueManager {
+ static async requeueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent) {
+ if (!(await Omnichannel.isWithinMACLimit(room))) {
+ logger.error({ msg: 'MAC limit reached, not routing inquiry', inquiry });
+ // We'll queue these inquiries so when new license is applied, they just start rolling again
+ // Minimizing disruption
+ await saveQueueInquiry(inquiry);
+ return;
+ }
+
+ const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
+ logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);
+ await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
+ const dbInquiry = await LivechatInquiry.findOneById(inquiry._id);
+
+ if (!dbInquiry) {
+ throw new Error('inquiry-not-found');
+ }
+
+ if (dbInquiry.status === 'ready') {
+ logger.debug(`Inquiry with id ${inquiry._id} is ready. Delegating to agent ${inquiryAgent?.username}`);
+ return RoutingManager.delegateInquiry(dbInquiry, inquiryAgent, undefined, room);
+ }
+ }
+
+ private static fnQueueInquiryStatus: (typeof QueueManager)['getInquiryStatus'] | undefined;
+
+ public static patchInquiryStatus(fn: (typeof QueueManager)['getInquiryStatus']) {
+ this.fnQueueInquiryStatus = fn;
+ }
+
+ static async getInquiryStatus({ room, agent }: { room: IOmnichannelRoom; agent?: SelectedAgent }): Promise {
+ if (this.fnQueueInquiryStatus) {
+ return this.fnQueueInquiryStatus({ room, agent });
+ }
+
+ if (!(await Omnichannel.isWithinMACLimit(room))) {
+ return LivechatInquiryStatus.QUEUED;
+ }
+
+ if (RoutingManager.getConfig()?.autoAssignAgent) {
+ return LivechatInquiryStatus.READY;
+ }
+
+ if (!agent || !(await allowAgentSkipQueue(agent))) {
+ return LivechatInquiryStatus.QUEUED;
+ }
+
+ return LivechatInquiryStatus.READY;
+ }
+
+ static async queueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent | null) {
+ if (inquiry.status === 'ready') {
+ return RoutingManager.delegateInquiry(inquiry, defaultAgent, undefined, room);
+ }
+
+ await callbacks.run('livechat.afterInquiryQueued', inquiry);
+
+ void callbacks.run('livechat.chatQueued', room);
+
+ await this.dispatchInquiryQueued(inquiry, room, defaultAgent);
+ }
+
+ static async requestRoom<
+ E extends Record & {
+ sla?: string;
+ customFields?: Record;
+ source?: OmnichannelSourceType;
+ },
+ >({
+ guest,
+ rid = Random.id(),
+ message,
+ roomInfo,
+ agent,
+ extraData: { customFields, ...extraData } = {} as E,
+ }: {
guest: ILivechatVisitor;
- message: Pick;
+ rid?: string;
+ message?: string;
roomInfo: {
source?: IOmnichannelRoom['source'];
[key: string]: unknown;
};
agent?: SelectedAgent;
- extraData?: Record;
- }) => Promise;
- unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise;
-};
-
-export const QueueManager: queueManager = {
- async requestRoom({ guest, message, roomInfo, agent, extraData }) {
+ extraData?: E;
+ }) {
logger.debug(`Requesting a room for guest ${guest._id}`);
- check(
- message,
- Match.ObjectIncluding({
- rid: String,
- }),
- );
check(
guest,
Match.ObjectIncluding({
@@ -99,29 +182,67 @@ export const QueueManager: queueManager = {
}),
);
- if (!(await checkServiceStatus({ guest, agent }))) {
- throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ const defaultAgent =
+ (await callbacks.run('livechat.beforeDelegateAgent', agent, {
+ department: guest.department,
+ })) || undefined;
+
+ const department = guest.department && (await getDepartment(guest.department));
+
+ /**
+ * we have 4 cases here
+ * 1. agent and no department
+ * 2. no agent and no department
+ * 3. no agent and department
+ * 4. agent and department informed
+ *
+ * in case 1, we check if the agent is online
+ * in case 2, we check if there is at least one online agent in the whole service
+ * in case 3, we check if there is at least one online agent in the department
+ *
+ * the case 4 is weird, but we are not throwing an error, just because the application works in some mysterious way
+ * we don't have explicitly defined what to do in this case so we just kept the old behavior
+ * it seems that agent has priority over department
+ * but some cases department is handled before agent
+ *
+ */
+
+ if (!settings.get('Livechat_accept_chats_with_no_agents')) {
+ if (agent && !defaultAgent) {
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ }
+
+ if (!defaultAgent && guest.department && !department) {
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ }
+
+ if (!agent && !guest.department && !(await Livechat.checkOnlineAgents())) {
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ }
}
- const { rid } = message;
const name = (roomInfo?.fname as string) || guest.name || guest.username;
- const room = await LivechatRooms.findOneById(await createLivechatRoom(rid, name, guest, roomInfo, extraData));
+ const room = await createLivechatRoom(rid, name, { ...guest, ...(department && { department }) }, roomInfo, {
+ ...extraData,
+ ...(Boolean(customFields) && { customFields }),
+ });
+
if (!room) {
logger.error(`Room for visitor ${guest._id} not found`);
throw new Error('room-not-found');
}
logger.debug(`Room for visitor ${guest._id} created with id ${room._id}`);
- const inquiry = await LivechatInquiry.findOneById(
- await createLivechatInquiry({
- rid,
- name,
- guest,
- message,
- extraData: { ...extraData, source: roomInfo.source },
- }),
- );
+ const inquiry = await createLivechatInquiry({
+ rid,
+ name,
+ initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }),
+ guest,
+ message,
+ extraData: { ...extraData, source: roomInfo.source },
+ });
+
if (!inquiry) {
logger.error(`Inquiry for visitor ${guest._id} not found`);
throw new Error('inquiry-not-found');
@@ -134,19 +255,28 @@ export const QueueManager: queueManager = {
void notifyOnSettingChanged(livechatSetting);
}
- await queueInquiry(inquiry, agent);
- logger.debug(`Inquiry ${inquiry._id} queued`);
-
- const newRoom = await LivechatRooms.findOneById(rid);
+ const newRoom = (await this.queueInquiry(inquiry, room, defaultAgent)) ?? (await LivechatRooms.findOneById(rid));
if (!newRoom) {
logger.error(`Room with id ${rid} not found`);
throw new Error('room-not-found');
}
+ if (!newRoom.servedBy && settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) {
+ const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({
+ inquiryId: inquiry._id,
+ department,
+ queueSortBy: getInquirySortMechanismSetting(),
+ });
+
+ if (inq) {
+ void dispatchInquiryPosition(inq);
+ }
+ }
+
return newRoom;
- },
+ }
- async unarchiveRoom(archivedRoom) {
+ static async unarchiveRoom(archivedRoom: IOmnichannelRoom) {
if (!archivedRoom) {
throw new Error('no-room-to-unarchive');
}
@@ -181,14 +311,70 @@ export const QueueManager: queueManager = {
if (!room) {
throw new Error('room-not-found');
}
- const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } }));
+ const inquiry = await createLivechatInquiry({
+ rid,
+ name,
+ guest,
+ message: message?.msg,
+ extraData: { source },
+ });
if (!inquiry) {
throw new Error('inquiry-not-found');
}
- await queueInquiry(inquiry, defaultAgent);
+ await this.requeueInquiry(inquiry, room, defaultAgent);
logger.debug(`Inquiry ${inquiry._id} queued`);
return room;
- },
-};
+ }
+
+ private static dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, agent?: SelectedAgent | null) => {
+ logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`);
+
+ const { department, rid, v } = inquiry;
+ // Alert only the online agents of the queued request
+ const onlineAgents = await Livechat.getOnlineAgents(department, agent);
+
+ if (!onlineAgents) {
+ logger.debug('Cannot notify agents of queued inquiry. No online agents found');
+ return;
+ }
+
+ logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`);
+ const notificationUserName = v && (v.name || v.username);
+
+ for await (const agent of onlineAgents) {
+ const { _id, active, emails, language, status, statusConnection, username } = agent;
+ await sendNotification({
+ // fake a subscription in order to make use of the function defined above
+ subscription: {
+ rid,
+ u: {
+ _id,
+ },
+ receiver: [
+ {
+ active,
+ emails,
+ language,
+ status,
+ statusConnection,
+ username,
+ },
+ ],
+ name: '',
+ },
+ sender: v,
+ hasMentionToAll: true, // consider all agents to be in the room
+ hasReplyToThread: false,
+ disableAllMessageNotifications: false,
+ hasMentionToHere: false,
+ message: { _id: '', u: v, msg: '' },
+ // we should use server's language for this type of messages instead of user's
+ notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language),
+ room: { ...room, name: i18n.t('New_chat_in_queue', {}, language) },
+ mentionIds: [],
+ });
+ }
+ };
+}
diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts
index 5782d01e318f..f4a2288305e5 100644
--- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts
+++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts
@@ -46,8 +46,8 @@ type Routing = {
inquiry: InquiryWithAgentInfo,
agent?: SelectedAgent | null,
options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } },
+ room?: IOmnichannelRoom,
): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>;
- assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise;
unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string, shouldQueue?: boolean): Promise;
takeInquiry(
inquiry: Omit<
@@ -55,11 +55,14 @@ type Routing = {
'estimatedInactivityCloseTimeAt' | 'message' | 't' | 'source' | 'estimatedWaitingTimeQueue' | 'priorityWeight' | '_updatedAt'
>,
agent: SelectedAgent | null,
- options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } },
+ options: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } },
+ room: IOmnichannelRoom,
): Promise;
transferRoom(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData): Promise;
delegateAgent(agent: SelectedAgent | undefined, inquiry: ILivechatInquiryRecord): Promise;
removeAllRoomSubscriptions(room: Pick, ignoreUser?: { _id: string }): Promise;
+
+ assignAgent(inquiry: InquiryWithAgentInfo, room: IOmnichannelRoom, agent: SelectedAgent): Promise;
};
export const RoutingManager: Routing = {
@@ -101,7 +104,7 @@ export const RoutingManager: Routing = {
return this.getMethod().getNextAgent(department, ignoreAgentId);
},
- async delegateInquiry(inquiry, agent, options = {}) {
+ async delegateInquiry(inquiry, agent, options = {}, room) {
const { department, rid } = inquiry;
logger.debug(`Attempting to delegate inquiry ${inquiry._id}`);
if (!agent || (agent.username && !(await Users.findOneOnlineAgentByUserList(agent.username)) && !(await allowAgentSkipQueue(agent)))) {
@@ -117,11 +120,15 @@ export const RoutingManager: Routing = {
return LivechatRooms.findOneById(rid);
}
+ if (!room) {
+ throw new Meteor.Error('error-invalid-room');
+ }
+
logger.debug(`Inquiry ${inquiry._id} will be taken by agent ${agent.agentId}`);
- return this.takeInquiry(inquiry, agent, options);
+ return this.takeInquiry(inquiry, agent, options, room);
},
- async assignAgent(inquiry, agent) {
+ async assignAgent(inquiry: InquiryWithAgentInfo, room: IOmnichannelRoom, agent: SelectedAgent): Promise {
check(
agent,
Match.ObjectIncluding({
@@ -142,19 +149,14 @@ export const RoutingManager: Routing = {
await Rooms.incUsersCountById(rid, 1);
const user = await Users.findOneById(agent.agentId);
- const room = await LivechatRooms.findOneById(rid);
if (user) {
await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]);
}
- if (!room) {
- logger.debug(`Cannot assign agent to inquiry ${inquiry._id}: Room not found`);
- throw new Meteor.Error('error-room-not-found', 'Room not found');
- }
-
await dispatchAgentDelegated(rid, agent.agentId);
- logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`);
+
+ logger.debug(`Agent ${agent.agentId} assigned to inquiry ${inquiry._id}. Instances notified`);
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user });
return inquiry;
@@ -206,7 +208,7 @@ export const RoutingManager: Routing = {
return true;
},
- async takeInquiry(inquiry, agent, options = { clientAction: false }) {
+ async takeInquiry(inquiry, agent, options = { clientAction: false }, room) {
check(
agent,
Match.ObjectIncluding({
@@ -227,7 +229,6 @@ export const RoutingManager: Routing = {
logger.debug(`Attempting to take Inquiry ${inquiry._id} [Agent ${agent.agentId}] `);
const { _id, rid } = inquiry;
- const room = await LivechatRooms.findOneById(rid);
if (!room?.open) {
logger.debug(`Cannot take Inquiry ${inquiry._id}: Room is closed`);
return room;
@@ -262,10 +263,16 @@ export const RoutingManager: Routing = {
await LivechatInquiry.takeInquiry(_id);
- const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent);
logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`);
- callbacks.runAsync('livechat.afterTakeInquiry', inq, agent);
+ callbacks.runAsync(
+ 'livechat.afterTakeInquiry',
+ {
+ inquiry: await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent),
+ room,
+ },
+ agent,
+ );
void notifyOnLivechatInquiryChangedById(inquiry._id, 'updated', {
status: LivechatInquiryStatus.TAKEN,
diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts
new file mode 100644
index 000000000000..c6acbbc5bcbd
--- /dev/null
+++ b/apps/meteor/app/livechat/server/lib/localTypes.ts
@@ -0,0 +1,31 @@
+import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings';
+
+export type GenericCloseRoomParams = {
+ room: IOmnichannelRoom;
+ comment?: string;
+ options?: {
+ clientAction?: boolean;
+ tags?: string[];
+ emailTranscript?:
+ | {
+ sendToVisitor: false;
+ }
+ | {
+ sendToVisitor: true;
+ requestData: NonNullable;
+ };
+ pdfTranscript?: {
+ requestedBy: string;
+ };
+ };
+};
+
+export type CloseRoomParamsByUser = {
+ user: IUser | null;
+} & GenericCloseRoomParams;
+
+export type CloseRoomParamsByVisitor = {
+ visitor: ILivechatVisitor;
+} & GenericCloseRoomParams;
+
+export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor;
diff --git a/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts b/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts
new file mode 100644
index 000000000000..76595a7ff640
--- /dev/null
+++ b/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts
@@ -0,0 +1,61 @@
+import type { ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
+import { LivechatVisitors, Users } from '@rocket.chat/models';
+
+import { settings } from '../../../settings/server';
+import type { CloseRoomParams } from './localTypes';
+
+export const parseTranscriptRequest = async (
+ room: IOmnichannelRoom,
+ options: CloseRoomParams['options'],
+ visitor?: ILivechatVisitor,
+ user?: IUser,
+): Promise => {
+ const visitorDecideTranscript = settings.get('Livechat_enable_transcript');
+ // visitor decides, no changes
+ if (visitorDecideTranscript) {
+ return options;
+ }
+
+ // send always is disabled, no changes
+ const sendAlways = settings.get('Livechat_transcript_send_always');
+ if (!sendAlways) {
+ return options;
+ }
+
+ const visitorData =
+ visitor ||
+ (await LivechatVisitors.findOneById>(room.v._id, { projection: { visitorEmails: 1 } }));
+ // no visitor, no changes
+ if (!visitorData) {
+ return options;
+ }
+ const visitorEmail = visitorData?.visitorEmails?.[0]?.address;
+ // visitor doesnt have email, no changes
+ if (!visitorEmail) {
+ return options;
+ }
+
+ const defOptions = { projection: { _id: 1, username: 1, name: 1 } };
+ const requestedBy =
+ user ||
+ (room.servedBy && (await Users.findOneById(room.servedBy._id, defOptions))) ||
+ (await Users.findOneById('rocket.cat', defOptions));
+
+ // no user available for backing request, no changes
+ if (!requestedBy) {
+ return options;
+ }
+
+ return {
+ ...options,
+ emailTranscript: {
+ sendToVisitor: true,
+ requestData: {
+ email: visitorEmail,
+ requestedAt: new Date(),
+ subject: '',
+ requestedBy,
+ },
+ },
+ };
+};
diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts
new file mode 100644
index 000000000000..74032121ee50
--- /dev/null
+++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts
@@ -0,0 +1,227 @@
+import { Message } from '@rocket.chat/core-services';
+import {
+ type IUser,
+ type MessageTypesValues,
+ type IOmnichannelSystemMessage,
+ isFileAttachment,
+ isFileImageAttachment,
+} from '@rocket.chat/core-typings';
+import colors from '@rocket.chat/fuselage-tokens/colors';
+import { Logger } from '@rocket.chat/logger';
+import { LivechatRooms, LivechatVisitors, Messages, Uploads, Users } from '@rocket.chat/models';
+import { check } from 'meteor/check';
+import moment from 'moment-timezone';
+
+import { callbacks } from '../../../../lib/callbacks';
+import { i18n } from '../../../../server/lib/i18n';
+import { FileUpload } from '../../../file-upload/server';
+import * as Mailer from '../../../mailer/server/api';
+import { settings } from '../../../settings/server';
+import { MessageTypes } from '../../../ui-utils/lib/MessageTypes';
+import { getTimezone } from '../../../utils/server/lib/getTimezone';
+
+const logger = new Logger('Livechat-SendTranscript');
+
+export async function sendTranscript({
+ token,
+ rid,
+ email,
+ subject,
+ user,
+}: {
+ token: string;
+ rid: string;
+ email: string;
+ subject?: string;
+ user?: Pick | null;
+}): Promise {
+ check(rid, String);
+ check(email, String);
+ logger.debug(`Sending conversation transcript of room ${rid} to user with token ${token}`);
+
+ const room = await LivechatRooms.findOneById(rid);
+
+ const visitor = await LivechatVisitors.getVisitorByToken(token, {
+ projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
+ });
+
+ if (!visitor) {
+ throw new Error('error-invalid-token');
+ }
+
+ // @ts-expect-error - Visitor typings should include language?
+ const userLanguage = visitor?.language || settings.get('Language') || 'en';
+ const timezone = getTimezone(user);
+ logger.debug(`Transcript will be sent using ${timezone} as timezone`);
+
+ if (!room) {
+ throw new Error('error-invalid-room');
+ }
+
+ // allow to only user to send transcripts from their own chats
+ if (room.t !== 'l' || !room.v || room.v.token !== token) {
+ throw new Error('error-invalid-room');
+ }
+
+ const showAgentInfo = settings.get('Livechat_show_agent_info');
+ const showSystemMessages = settings.get('Livechat_transcript_show_system_messages');
+ const closingMessage = await Messages.findLivechatClosingMessage(rid, { projection: { ts: 1 } });
+ const ignoredMessageTypes: MessageTypesValues[] = [
+ 'livechat_navigation_history',
+ 'livechat_transcript_history',
+ 'command',
+ 'livechat-close',
+ 'livechat-started',
+ 'livechat_video_call',
+ 'omnichannel_priority_change_history',
+ ];
+ const acceptableImageMimeTypes = ['image/jpeg', 'image/png', 'image/jpg'];
+ const messages = await Messages.findVisibleByRoomIdNotContainingTypesBeforeTs(
+ rid,
+ ignoredMessageTypes,
+ closingMessage?.ts ? new Date(closingMessage.ts) : new Date(),
+ showSystemMessages,
+ {
+ sort: { ts: 1 },
+ },
+ );
+
+ let html = '
';
+ const InvalidFileMessage = `
${i18n.t(
+ 'This_attachment_is_not_supported',
+ { lng: userLanguage },
+ )}
`;
+
+ for await (const message of messages) {
+ let author;
+ if (message.u._id === visitor._id) {
+ author = i18n.t('You', { lng: userLanguage });
+ } else {
+ author = showAgentInfo ? message.u.name || message.u.username : i18n.t('Agent', { lng: userLanguage });
+ }
+
+ const isSystemMessage = MessageTypes.isSystemMessage(message);
+ const messageType = isSystemMessage && MessageTypes.getType(message);
+
+ let messageContent = messageType
+ ? `
${i18n.t(
+ messageType.message,
+ messageType.data
+ ? { ...messageType.data(message), interpolation: { escapeValue: false } }
+ : { interpolation: { escapeValue: false } },
+ )}`
+ : message.msg;
+
+ let filesHTML = '';
+
+ if (message.attachments && message.attachments?.length > 0) {
+ messageContent = message.attachments[0].description || '';
+
+ for await (const attachment of message.attachments) {
+ if (!isFileAttachment(attachment)) {
+ // ignore other types of attachments
+ continue;
+ }
+
+ if (!isFileImageAttachment(attachment)) {
+ filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`;
+ continue;
+ }
+
+ if (!attachment.image_type || !acceptableImageMimeTypes.includes(attachment.image_type)) {
+ filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`;
+ continue;
+ }
+
+ // Image attachment can be rendered in email body
+ const file = message.files?.find((file) => file.name === attachment.title);
+
+ if (!file) {
+ filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`;
+ continue;
+ }
+
+ const uploadedFile = await Uploads.findOneById(file._id);
+
+ if (!uploadedFile) {
+ filesHTML += `
${file.name}${InvalidFileMessage}
`;
+ continue;
+ }
+
+ const uploadedFileBuffer = await FileUpload.getBuffer(uploadedFile);
+ filesHTML += `
${file.name}
`;
+ }
+ }
+
+ const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL');
+ const singleMessage = `
+
${author} ${datetime}
+
${messageContent}
+
${filesHTML}
+ `;
+ html += singleMessage;
+ }
+
+ html = `${html}
`;
+
+ const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);
+ let emailFromRegexp = '';
+ if (fromEmail) {
+ emailFromRegexp = fromEmail[0];
+ } else {
+ emailFromRegexp = settings.get('From_Email');
+ }
+
+ // Some endpoints allow the caller to pass a different `subject` via parameter.
+ // IF subject is passed, we'll use that one and treat it as an override
+ // IF no subject is passed, we fallback to the setting `Livechat_transcript_email_subject`
+ // IF that is not configured, we fallback to 'Transcript of your livechat conversation', which is the default value
+ // As subject and setting value are user input, we don't translate them
+ const mailSubject =
+ subject ||
+ settings.get('Livechat_transcript_email_subject') ||
+ i18n.t('Transcript_of_your_livechat_conversation', { lng: userLanguage });
+
+ await Mailer.send({
+ to: email,
+ from: emailFromRegexp,
+ replyTo: emailFromRegexp,
+ subject: mailSubject,
+ html,
+ });
+
+ setImmediate(() => {
+ void callbacks.run('livechat.sendTranscript', messages, email);
+ });
+
+ const requestData: IOmnichannelSystemMessage['requestData'] = {
+ type: 'user',
+ visitor,
+ user,
+ };
+
+ if (!user?.username) {
+ const cat = await Users.findOneById('rocket.cat', { projection: { _id: 1, username: 1, name: 1 } });
+ if (cat) {
+ requestData.user = cat;
+ requestData.type = 'visitor';
+ }
+ }
+
+ if (!requestData.user) {
+ logger.error('rocket.cat user not found');
+ throw new Error('No user provided and rocket.cat not found');
+ }
+
+ await Message.saveSystemMessage('livechat_transcript_history', room._id, '', requestData.user, {
+ requestData,
+ });
+
+ return true;
+}
diff --git a/apps/meteor/app/livechat/server/methods/registerGuest.ts b/apps/meteor/app/livechat/server/methods/registerGuest.ts
index 01f720b85a4d..4a531d0c89e5 100644
--- a/apps/meteor/app/livechat/server/methods/registerGuest.ts
+++ b/apps/meteor/app/livechat/server/methods/registerGuest.ts
@@ -23,21 +23,24 @@ declare module '@rocket.chat/ui-contexts' {
department?: string;
customFields?: Array<{ key: string; value: string; overwrite: boolean; scope?: unknown }>;
}): {
- userId: string;
- visitor: ILivechatVisitor | null;
+ userId: ILivechatVisitor['_id'];
+ visitor: Pick;
};
}
}
Meteor.methods({
- async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) {
+ async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}): Promise<{
+ userId: ILivechatVisitor['_id'];
+ visitor: Pick;
+ }> {
methodDeprecationLogger.method('livechat:registerGuest', '7.0.0');
if (!token) {
throw new Meteor.Error('error-invalid-token', 'Invalid token', { method: 'livechat:registerGuest' });
}
- const userId = await LivechatTyped.registerGuest.call(this, {
+ const visitor = await LivechatTyped.registerGuest.call(this, {
token,
name,
email,
@@ -47,16 +50,6 @@ Meteor.methods({
// update visited page history to not expire
await Messages.keepHistoryForToken(token);
- const visitor = await LivechatVisitors.getVisitorByToken(token, {
- projection: {
- token: 1,
- name: 1,
- username: 1,
- visitorEmails: 1,
- department: 1,
- },
- });
-
if (!visitor) {
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:registerGuest' });
}
@@ -89,8 +82,15 @@ Meteor.methods({
}
return {
- userId,
- visitor,
+ userId: visitor._id,
+ visitor: {
+ _id: visitor._id,
+ token: visitor.token,
+ name: visitor.name,
+ username: visitor.username,
+ visitorEmails: visitor.visitorEmails,
+ department: visitor.department,
+ },
};
},
});
diff --git a/apps/meteor/app/livechat/server/methods/takeInquiry.ts b/apps/meteor/app/livechat/server/methods/takeInquiry.ts
index 3433b4a33ae8..30a5dabb5717 100644
--- a/apps/meteor/app/livechat/server/methods/takeInquiry.ts
+++ b/apps/meteor/app/livechat/server/methods/takeInquiry.ts
@@ -60,7 +60,7 @@ export const takeInquiry = async (
};
try {
- await RoutingManager.takeInquiry(inquiry, agent, options);
+ await RoutingManager.takeInquiry(inquiry, agent, options ?? {}, room);
} catch (e: any) {
throw new Meteor.Error(e.message);
}
diff --git a/apps/meteor/app/markdown/lib/markdown.js b/apps/meteor/app/markdown/lib/markdown.js
index 3c3acdb17893..c7fe452e0829 100644
--- a/apps/meteor/app/markdown/lib/markdown.js
+++ b/apps/meteor/app/markdown/lib/markdown.js
@@ -69,6 +69,7 @@ class MarkdownClass {
return code(...args);
}
+ /** @param {string} message */
filterMarkdownFromMessage(message) {
return parsers.filtered(message);
}
@@ -76,6 +77,7 @@ class MarkdownClass {
export const Markdown = new MarkdownClass();
+/** @param {string} message */
export const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message);
export const createMarkdownMessageRenderer = ({ ...options }) => {
diff --git a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js
index ac53144d6d1b..260fc835d8a0 100644
--- a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js
+++ b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js
@@ -1,6 +1,7 @@
-/*
+/**
* Filter markdown tags in message
- * Use case: notifications
+ * Use case: notifications
+ * @param {string} message
*/
export const filtered = (
message,
diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
index 76747b599104..6e68518ef31c 100644
--- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
+++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
@@ -198,11 +198,6 @@ export class SAML {
updateData.emails = emails;
}
- // Overwrite fullname if needed
- if (nameOverwrite === true) {
- updateData.name = fullName;
- }
-
// When updating an user, we only update the roles if we received them from the mapping
if (userObject.roles?.length) {
updateData.roles = userObject.roles;
@@ -221,8 +216,8 @@ export class SAML {
},
);
- if ((username && username !== user.username) || (fullName && fullName !== user.name)) {
- await saveUserIdentity({ _id: user._id, name: fullName || undefined, username });
+ if ((username && username !== user.username) || (nameOverwrite && fullName && fullName !== user.name)) {
+ await saveUserIdentity({ _id: user._id, name: nameOverwrite ? fullName || undefined : user.name, username });
}
// sending token along with the userId
diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts
index 87ced6e130df..6015780f118f 100644
--- a/apps/meteor/app/push/server/fcm.ts
+++ b/apps/meteor/app/push/server/fcm.ts
@@ -1,6 +1,6 @@
+import { serverFetch as fetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch';
import EJSON from 'ejson';
-import fetch from 'node-fetch';
-import type { RequestInit, Response } from 'node-fetch';
+import type { Response } from 'node-fetch';
import type { PendingPushNotification } from './definition';
import { logger } from './logger';
@@ -65,7 +65,7 @@ type FCMError = {
* - For 429 errors: retry after waiting for the duration set in the retry-after header. If no retry-after header is set, default to 60 seconds.
* - For 500 errors: retry with exponential backoff.
*/
-async function fetchWithRetry(url: string, _removeToken: () => void, options: RequestInit, retries = 0): Promise {
+async function fetchWithRetry(url: string, _removeToken: () => void, options: ExtendedFetchOptions, retries = 0): Promise {
const MAX_RETRIES = 5;
const response = await fetch(url, options);
diff --git a/apps/meteor/app/settings/server/CachedSettings.ts b/apps/meteor/app/settings/server/CachedSettings.ts
index 06cfad4a91a6..9a42569b4cf6 100644
--- a/apps/meteor/app/settings/server/CachedSettings.ts
+++ b/apps/meteor/app/settings/server/CachedSettings.ts
@@ -333,7 +333,7 @@ export class CachedSettings
}
public getConfig = (config?: OverCustomSettingsConfig): SettingsConfig => ({
- debounce: 500,
+ debounce: process.env.TEST_MODE ? 0 : 500,
...config,
});
diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts
index 5783e2946dc1..d7d2fa0a79f8 100644
--- a/apps/meteor/app/settings/server/SettingsRegistry.ts
+++ b/apps/meteor/app/settings/server/SettingsRegistry.ts
@@ -73,7 +73,7 @@ const compareSettingsIgnoringKeys =
.filter((key) => !keys.includes(key as keyof ISetting))
.every((key) => isEqual(a[key as keyof ISetting], b[key as keyof ISetting]));
-const compareSettings = compareSettingsIgnoringKeys([
+export const compareSettings = compareSettingsIgnoringKeys([
'value',
'ts',
'createdAt',
@@ -139,6 +139,7 @@ export class SettingsRegistry {
const settingFromCodeOverwritten = overwriteSetting(settingFromCode);
const settingStored = this.store.getSetting(_id);
+
const settingStoredOverwritten = settingStored && overwriteSetting(settingStored);
try {
@@ -166,6 +167,10 @@ export class SettingsRegistry {
})();
await this.saveUpdatedSetting(_id, updatedProps, removedKeys);
+ if ('value' in updatedProps) {
+ this.store.set(updatedProps as ISetting);
+ }
+
return;
}
@@ -175,6 +180,7 @@ export class SettingsRegistry {
const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key));
await this.saveUpdatedSetting(_id, settingProps, removedKeys);
+ this.store.set(settingFromCodeOverwritten);
}
return;
}
diff --git a/apps/meteor/app/settings/server/functions/settings.mocks.ts b/apps/meteor/app/settings/server/functions/settings.mocks.ts
index 9cd409ba0b83..fb31c3021b1b 100644
--- a/apps/meteor/app/settings/server/functions/settings.mocks.ts
+++ b/apps/meteor/app/settings/server/functions/settings.mocks.ts
@@ -9,6 +9,12 @@ type Dictionary = {
class SettingsClass {
settings: ICachedSettings;
+ private delay = 0;
+
+ setDelay(delay: number): void {
+ this.delay = delay;
+ }
+
find(): any[] {
return [];
}
@@ -65,22 +71,41 @@ class SettingsClass {
throw new Error('Invalid upsert');
}
- // console.log(query, data);
- this.data.set(query._id, data);
-
- // Can't import before the mock command on end of this file!
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- this.settings.set(data);
+ if (this.delay) {
+ setTimeout(() => {
+ // console.log(query, data);
+ this.data.set(query._id, data);
+
+ // Can't import before the mock command on end of this file!
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ this.settings.set(data);
+ }, this.delay);
+ } else {
+ this.data.set(query._id, data);
+ // Can't import before the mock command on end of this file!
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ this.settings.set(data);
+ }
this.upsertCalls++;
}
+ findOneAndUpdate({ _id }: { _id: string }, value: any, options?: any) {
+ this.updateOne({ _id }, value, options);
+ return { value: this.findOne({ _id }) };
+ }
+
updateValueById(id: string, value: any): void {
this.data.set(id, { ...this.data.get(id), value });
-
// Can't import before the mock command on end of this file!
// eslint-disable-next-line @typescript-eslint/no-var-requires
- this.settings.set(this.data.get(id) as ISetting);
+ if (this.delay) {
+ setTimeout(() => {
+ this.settings.set(this.data.get(id) as ISetting);
+ }, this.delay);
+ } else {
+ this.settings.set(this.data.get(id) as ISetting);
+ }
}
}
diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js
index 78d48deb4993..0263d5369a4c 100644
--- a/apps/meteor/app/slackbridge/server/SlackAdapter.js
+++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js
@@ -1341,7 +1341,7 @@ export default class SlackAdapter {
const user = (await this.rocket.findUser(member)) || (await this.rocket.addUser(member));
if (user) {
slackLogger.debug('Adding user to room', user.username, rid);
- await addUserToRoom(rid, user, null, true);
+ await addUserToRoom(rid, user, null, { skipSystemMessage: true });
}
}
}
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 cead4a2cb584..20b023cc61aa 100644
--- a/apps/meteor/app/theme/client/imports/general/base_old.css
+++ b/apps/meteor/app/theme/client/imports/general/base_old.css
@@ -777,7 +777,7 @@
}
& .start {
- margin-top: 12px;
+ margin-top: 44px;
text-align: center;
@@ -794,12 +794,6 @@
& .editing .body {
border-radius: var(--border-radius);
}
-
- &.has-leader {
- & .wrapper {
- padding-top: 57px;
- }
- }
}
.rcx-message {
diff --git a/apps/meteor/app/utils/lib/mimeTypes.spec.ts b/apps/meteor/app/utils/lib/mimeTypes.spec.ts
new file mode 100644
index 000000000000..d0fbd4360e24
--- /dev/null
+++ b/apps/meteor/app/utils/lib/mimeTypes.spec.ts
@@ -0,0 +1,89 @@
+import { expect } from 'chai';
+
+import { getExtension, getMimeType } from './mimeTypes';
+
+const mimeTypeToExtension = {
+ 'text/plain': 'txt',
+ 'image/x-icon': 'ico',
+ 'image/vnd.microsoft.icon': 'ico',
+ 'image/png': 'png',
+ 'image/jpeg': 'jpeg',
+ 'image/gif': 'gif',
+ 'image/webp': 'webp',
+ 'image/svg+xml': 'svg',
+ 'image/bmp': 'bmp',
+ 'image/tiff': 'tif',
+ 'audio/wav': 'wav',
+ 'audio/wave': 'wav',
+ 'audio/aac': 'aac',
+ 'audio/x-aac': 'aac',
+ 'audio/mp4': 'm4a',
+ 'audio/mpeg': 'mpga',
+ 'audio/ogg': 'oga',
+ 'application/octet-stream': 'bin',
+};
+
+const extensionToMimeType = {
+ lst: 'text/plain',
+ txt: 'text/plain',
+ ico: 'image/x-icon',
+ png: 'image/png',
+ jpeg: 'image/jpeg',
+ gif: 'image/gif',
+ webp: 'image/webp',
+ svg: 'image/svg+xml',
+ bmp: 'image/bmp',
+ tiff: 'image/tiff',
+ tif: 'image/tiff',
+ wav: 'audio/wav',
+ aac: 'audio/aac',
+ mp3: 'audio/mpeg',
+ ogg: 'audio/ogg',
+ oga: 'audio/ogg',
+ m4a: 'audio/mp4',
+ mpga: 'audio/mpeg',
+ mp4: 'video/mp4',
+ bin: 'application/octet-stream',
+};
+
+describe('mimeTypes', () => {
+ describe('getExtension', () => {
+ for (const [mimeType, extension] of Object.entries(mimeTypeToExtension)) {
+ it(`should return the correct extension ${extension} for the given mimeType ${mimeType}`, async () => {
+ expect(getExtension(mimeType)).to.be.eql(extension);
+ });
+ }
+
+ it('should return an empty string if the mimeType is not found', async () => {
+ expect(getExtension('application/unknown')).to.be.eql('');
+ });
+ });
+
+ describe('getMimeType', () => {
+ for (const [extension, mimeType] of Object.entries(extensionToMimeType)) {
+ it(`should return the correct mimeType ${mimeType} for the given fileName file.${extension} passing the correct mimeType`, async () => {
+ expect(getMimeType(mimeType, `file.${extension}`)).to.be.eql(mimeType);
+ });
+ }
+
+ it('should return the correct mimeType for the given fileName', async () => {
+ for (const [extension, mimeType] of Object.entries(extensionToMimeType)) {
+ expect(getMimeType('application/unknown', `file.${extension}`)).to.be.eql(mimeType);
+ }
+ });
+
+ it('should return the correct mimeType for the given fileName when informed mimeType is application/octet-stream', async () => {
+ for (const [extension, mimeType] of Object.entries(extensionToMimeType)) {
+ expect(getMimeType('application/octet-stream', `file.${extension}`)).to.be.eql(mimeType);
+ }
+ });
+
+ it('should return the mimeType if it is not application/octet-stream', async () => {
+ expect(getMimeType('audio/wav', 'file.wav')).to.be.eql('audio/wav');
+ });
+
+ it('should return application/octet-stream if the mimeType is not found', async () => {
+ expect(getMimeType('application/octet-stream', 'file.unknown')).to.be.eql('application/octet-stream');
+ });
+ });
+});
diff --git a/apps/meteor/app/utils/lib/mimeTypes.ts b/apps/meteor/app/utils/lib/mimeTypes.ts
index 909a955d6724..df670145b494 100644
--- a/apps/meteor/app/utils/lib/mimeTypes.ts
+++ b/apps/meteor/app/utils/lib/mimeTypes.ts
@@ -3,8 +3,8 @@ import mime from 'mime-type/with-db';
mime.types.wav = 'audio/wav';
mime.types.lst = 'text/plain';
mime.define('image/vnd.microsoft.icon', { source: '', extensions: ['ico'] }, mime.dupAppend);
-mime.define('image/x-icon', { source: '', extensions: ['ico'] }, mime.dupAppend);
-mime.types.ico = 'image/x-icon';
+mime.define('image/x-icon', { source: '', extensions: ['ico'] }, mime.dupOverwrite);
+mime.define('audio/aac', { source: '', extensions: ['aac'] }, mime.dupOverwrite);
const getExtension = (param: string): string => {
const extension = mime.extension(param);
@@ -12,7 +12,14 @@ const getExtension = (param: string): string => {
return !extension || typeof extension === 'boolean' ? '' : extension;
};
-const getMimeType = (fileName: string): string => {
+const getMimeType = (mimetype: string, fileName: string): string => {
+ // If the extension from the mimetype is different from the file extension, the file
+ // extension may be wrong so use the informed mimetype
+ const extension = mime.extension(mimetype);
+ if (mimetype !== 'application/octet-stream' && extension && extension !== fileName.split('.').pop()) {
+ return mimetype;
+ }
+
const fileMimeType = mime.lookup(fileName);
return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream';
};
diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info
index 3cb06b1e99ab..7cad52f21bcf 100644
--- a/apps/meteor/app/utils/rocketchat.info
+++ b/apps/meteor/app/utils/rocketchat.info
@@ -1,3 +1,3 @@
{
- "version": "6.10.2"
+ "version": "6.11.0-rc.6"
}
diff --git a/apps/meteor/client/NavBarV2/NavBar.tsx b/apps/meteor/client/NavBarV2/NavBar.tsx
new file mode 100644
index 000000000000..908e729c956e
--- /dev/null
+++ b/apps/meteor/client/NavBarV2/NavBar.tsx
@@ -0,0 +1,73 @@
+import { useToolbar } from '@react-aria/toolbar';
+import { NavBar as NavBarComponent, NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage';
+import { usePermission, useTranslation, useUser } from '@rocket.chat/ui-contexts';
+import React, { useRef } from 'react';
+
+import { useIsCallEnabled, useIsCallReady } from '../contexts/CallContext';
+import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled';
+import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink';
+import { useHasLicenseModule } from '../hooks/useHasLicenseModule';
+import {
+ NavBarItemOmniChannelCallDialPad,
+ NavBarItemOmnichannelContact,
+ NavBarItemOmnichannelLivechatToggle,
+ NavBarItemOmnichannelQueue,
+ NavBarItemOmnichannelCallToggle,
+} from './NavBarOmnichannelToolbar';
+import { NavBarItemMarketPlaceMenu, NavBarItemAuditMenu, NavBarItemDirectoryPage, NavBarItemHomePage } from './NavBarPagesToolbar';
+import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from './NavBarSettingsToolbar';
+
+const NavBar = () => {
+ const t = useTranslation();
+ const user = useUser();
+
+ const hasAuditLicense = useHasLicenseModule('auditing') === true;
+
+ const showOmnichannel = useOmnichannelEnabled();
+ const hasManageAppsPermission = usePermission('manage-apps');
+ const hasAccessMarketplacePermission = usePermission('access-marketplace');
+ const showMarketplace = hasAccessMarketplacePermission || hasManageAppsPermission;
+
+ const showOmnichannelQueueLink = useOmnichannelShowQueueLink();
+ const isCallEnabled = useIsCallEnabled();
+ const isCallReady = useIsCallReady();
+
+ const pagesToolbarRef = useRef(null);
+ const { toolbarProps: pagesToolbarProps } = useToolbar({ 'aria-label': t('Pages') }, pagesToolbarRef);
+
+ const omnichannelToolbarRef = useRef(null);
+ const { toolbarProps: omnichannelToolbarProps } = useToolbar({ 'aria-label': t('Omnichannel') }, omnichannelToolbarRef);
+
+ return (
+
+
+