diff --git a/.changeset/blue-ladybugs-raise.md b/.changeset/blue-ladybugs-raise.md deleted file mode 100644 index 44d7a06b4111..000000000000 --- a/.changeset/blue-ladybugs-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Translation files are requested multiple times diff --git a/.changeset/brave-snakes-scream.md b/.changeset/brave-snakes-scream.md new file mode 100644 index 000000000000..914f248cd821 --- /dev/null +++ b/.changeset/brave-snakes-scream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where broadcasted events were published twice within the same instance diff --git a/.changeset/breezy-bugs-jam.md b/.changeset/breezy-bugs-jam.md deleted file mode 100644 index 7e7cc7b8283b..000000000000 --- a/.changeset/breezy-bugs-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Managers allowed to make deactivated agent's available diff --git a/.changeset/bright-carpets-fly.md b/.changeset/bright-carpets-fly.md deleted file mode 100644 index 6a8ac2608569..000000000000 --- a/.changeset/bright-carpets-fly.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -new: ring mobile users on direct conference calls diff --git a/.changeset/bright-snakes-vanish.md b/.changeset/bright-snakes-vanish.md deleted file mode 100644 index f198bfe93ae9..000000000000 --- a/.changeset/bright-snakes-vanish.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue causing `queue time` to be calculated from current time when a room was closed without being served. -Now: -- For served rooms: queue time = servedBy time - queuedAt -- For not served, but open rooms = now - queuedAt -- For not served and closed rooms = closedAt - queuedAt diff --git a/.changeset/brown-clouds-add.md b/.changeset/brown-clouds-add.md deleted file mode 100644 index 6b69289177b5..000000000000 --- a/.changeset/brown-clouds-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Performance issue on `Messages.countByType` aggregation caused by unindexed property on messages collection diff --git a/.changeset/brown-comics-cheat.md b/.changeset/brown-comics-cheat.md new file mode 100644 index 000000000000..a7907979881b --- /dev/null +++ b/.changeset/brown-comics-cheat.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/model-typings": patch +--- + +chore: Calculate & Store MAC stats +Added new info to the stats: `omnichannelContactsBySource`, `uniqueContactsOfLastMonth`, `uniqueContactsOfLastWeek`, `uniqueContactsOfYesterday` diff --git a/.changeset/chilled-flies-fold.md b/.changeset/chilled-flies-fold.md deleted file mode 100644 index 17a0f9eb6dc5..000000000000 --- a/.changeset/chilled-flies-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -feat: add sections to room header and user infos menus with menuV2 diff --git a/.changeset/chilled-phones-give.md b/.changeset/chilled-phones-give.md deleted file mode 100644 index cb0887db0883..000000000000 --- a/.changeset/chilled-phones-give.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch -"@rocket.chat/rest-typings": patch ---- - -Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints diff --git a/.changeset/cool-students-tan.md b/.changeset/cool-students-tan.md deleted file mode 100644 index 07760541628a..000000000000 --- a/.changeset/cool-students-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -feat(apps): `ActionManagerBusyState` component for apps `ui.interaction` diff --git a/.changeset/cuddly-houses-tie.md b/.changeset/cuddly-houses-tie.md deleted file mode 100644 index 76d86a690388..000000000000 --- a/.changeset/cuddly-houses-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Hide Reset TOTP option if 2FA is disabled diff --git a/.changeset/cuddly-ties-bake.md b/.changeset/cuddly-ties-bake.md deleted file mode 100644 index d912d2969d75..000000000000 --- a/.changeset/cuddly-ties-bake.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": minor -"@rocket.chat/uikit-playground": minor ---- - -feat: Add missing variants to UIKit button diff --git a/.changeset/curly-shoes-burn.md b/.changeset/curly-shoes-burn.md deleted file mode 100644 index 67d453ab7245..000000000000 --- a/.changeset/curly-shoes-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Added ability to freeze or completely disable integration scripts through envvars diff --git a/.changeset/custom-emoji-fs.md b/.changeset/custom-emoji-fs.md deleted file mode 100644 index a9a797f35bc8..000000000000 --- a/.changeset/custom-emoji-fs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: custom emoji upload with FileSystem method diff --git a/.changeset/dropdown.md b/.changeset/dropdown.md deleted file mode 100644 index 935c12aebe85..000000000000 --- a/.changeset/dropdown.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -New filters to the Rooms Table at `Workspace > Rooms` diff --git a/.changeset/dull-trainers-drive.md b/.changeset/dull-trainers-drive.md new file mode 100644 index 000000000000..f5a673cd8c30 --- /dev/null +++ b/.changeset/dull-trainers-drive.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Remove model-level query restrictions for monitors diff --git a/.changeset/eighty-kids-jog.md b/.changeset/eighty-kids-jog.md deleted file mode 100644 index 6410813d80a6..000000000000 --- a/.changeset/eighty-kids-jog.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/livechat': minor -'@rocket.chat/meteor': minor ---- - -Added new Omnichannel's trigger condition "After starting a chat". diff --git a/.changeset/eleven-icons-tan.md b/.changeset/eleven-icons-tan.md deleted file mode 100644 index c51124c05dd2..000000000000 --- a/.changeset/eleven-icons-tan.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-typings": minor -"@rocket.chat/model-typings": minor -"@rocket.chat/rest-typings": minor ---- - -Introduce the ability to report an user diff --git a/.changeset/empty-ants-enjoy.md b/.changeset/empty-ants-enjoy.md deleted file mode 100644 index 4a55f82d0abf..000000000000 --- a/.changeset/empty-ants-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -fix: Wrong toast message while creating a new custom sound with an existing name diff --git a/.changeset/fair-cats-destroy.md b/.changeset/fair-cats-destroy.md deleted file mode 100644 index 7dfb74955a94..000000000000 --- a/.changeset/fair-cats-destroy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -When setting a room as read-only, do not allow previously unmuted users to send messages. diff --git a/.changeset/fast-pumpkins-smoke.md b/.changeset/fast-pumpkins-smoke.md deleted file mode 100644 index 2374776bf3b5..000000000000 --- a/.changeset/fast-pumpkins-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fix: finnish translation diff --git a/.changeset/fast-yaks-collect.md b/.changeset/fast-yaks-collect.md deleted file mode 100644 index 60dd92030163..000000000000 --- a/.changeset/fast-yaks-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue where 2fa was not working after an OAuth redirect diff --git a/.changeset/fifty-cars-divide.md b/.changeset/fifty-cars-divide.md deleted file mode 100644 index 6c09cf6869c8..000000000000 --- a/.changeset/fifty-cars-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed issue with custom OAuth services' settings not being be fully removed diff --git a/.changeset/fluffy-beds-buy.md b/.changeset/fluffy-beds-buy.md deleted file mode 100644 index f90513b946c3..000000000000 --- a/.changeset/fluffy-beds-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Added support for threaded conversation in Federated rooms. diff --git a/.changeset/fluffy-lions-rage.md b/.changeset/fluffy-lions-rage.md deleted file mode 100644 index 09437a2cb88e..000000000000 --- a/.changeset/fluffy-lions-rage.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -chore: (Livechat) Replace all `dangerouslySetInnerHTML` with `gazzodown` diff --git a/.changeset/fluffy-monkeys-sing.md b/.changeset/fluffy-monkeys-sing.md new file mode 100644 index 000000000000..db93491b0ecd --- /dev/null +++ b/.changeset/fluffy-monkeys-sing.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Changed the name of the administration Logs page to "Records", implemented a tab layout in this page and added a new tab called "Analytic reports" that shows the most recent result of the statistics endpoint. diff --git a/.changeset/forty-hotels-pretend.md b/.changeset/forty-hotels-pretend.md deleted file mode 100644 index b23825d5a02a..000000000000 --- a/.changeset/forty-hotels-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times diff --git a/.changeset/four-parents-cheer.md b/.changeset/four-parents-cheer.md deleted file mode 100644 index 2fbb8e2b279f..000000000000 --- a/.changeset/four-parents-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -load sounds right before playing them diff --git a/.changeset/friendly-glasses-mate.md b/.changeset/friendly-glasses-mate.md deleted file mode 100644 index 6a7a7b4f8546..000000000000 --- a/.changeset/friendly-glasses-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -fix: Time format of Retention Policy diff --git a/.changeset/fuzzy-glasses-divide.md b/.changeset/fuzzy-glasses-divide.md deleted file mode 100644 index cf77bbde5507..000000000000 --- a/.changeset/fuzzy-glasses-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Added a new Roles bridge to RC Apps-Engine for reading and retrieving role details. diff --git a/.changeset/fuzzy-schools-brake.md b/.changeset/fuzzy-schools-brake.md deleted file mode 100644 index c6af54a2ef57..000000000000 --- a/.changeset/fuzzy-schools-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix users being created without the `roles` field diff --git a/.changeset/gentle-radios-relate.md b/.changeset/gentle-radios-relate.md new file mode 100644 index 000000000000..8d5f12b3a286 --- /dev/null +++ b/.changeset/gentle-radios-relate.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed DM room with "guest" user kept as "read only" after reactivating user diff --git a/.changeset/gold-horses-pretend.md b/.changeset/gold-horses-pretend.md deleted file mode 100644 index a8908b68a23e..000000000000 --- a/.changeset/gold-horses-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed CAS login after popup closes diff --git a/.changeset/gold-moose-press.md b/.changeset/gold-moose-press.md deleted file mode 100644 index 605fb7c649ea..000000000000 --- a/.changeset/gold-moose-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix moment timestamps language change diff --git a/.changeset/good-elephants-live.md b/.changeset/good-elephants-live.md deleted file mode 100644 index 8cb3e9d87fc4..000000000000 --- a/.changeset/good-elephants-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed message fetching method in LivechatBridge for Apps diff --git a/.changeset/green-adults-peel.md b/.changeset/green-adults-peel.md deleted file mode 100644 index b07f5ea3e6bf..000000000000 --- a/.changeset/green-adults-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix pruning messages in a room results in an incorrect message counter diff --git a/.changeset/grumpy-candles-rule.md b/.changeset/grumpy-candles-rule.md deleted file mode 100644 index 28673ce91a73..000000000000 --- a/.changeset/grumpy-candles-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fix: rejected conference calls continue to ring diff --git a/.changeset/heavy-ads-carry.md b/.changeset/heavy-ads-carry.md new file mode 100644 index 000000000000..c04e52fb48a0 --- /dev/null +++ b/.changeset/heavy-ads-carry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Change plan name from Enterprise to Premium on marketplace filtering diff --git a/.changeset/heavy-baboons-laugh.md b/.changeset/heavy-baboons-laugh.md deleted file mode 100644 index 5c32965dcf62..000000000000 --- a/.changeset/heavy-baboons-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -User information crashing for some locales diff --git a/.changeset/heavy-cougars-marry.md b/.changeset/heavy-cougars-marry.md deleted file mode 100644 index 893f53352114..000000000000 --- a/.changeset/heavy-cougars-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix performance issue on Engagement Dashboard aggregation diff --git a/.changeset/hip-hounds-ring.md b/.changeset/hip-hounds-ring.md deleted file mode 100644 index 79dfba6dd031..000000000000 --- a/.changeset/hip-hounds-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Added ability to disable private app installation via envvar (DISABLE_PRIVATE_APP_INSTALLATION) diff --git a/.changeset/hip-mugs-promise.md b/.changeset/hip-mugs-promise.md deleted file mode 100644 index 7100fec026e3..000000000000 --- a/.changeset/hip-mugs-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -improve: System messages for omni-visitor abandonment feature diff --git a/.changeset/honest-glasses-roll.md b/.changeset/honest-glasses-roll.md deleted file mode 100644 index 679f46fb8420..000000000000 --- a/.changeset/honest-glasses-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -chore: Add danger variant to apps action button menus diff --git a/.changeset/honest-numbers-compete.md b/.changeset/honest-numbers-compete.md deleted file mode 100644 index 1fd017e7fc16..000000000000 --- a/.changeset/honest-numbers-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes SAML full name updates not being mirrored to DM rooms. diff --git a/.changeset/importer-progress-bar.md b/.changeset/importer-progress-bar.md deleted file mode 100644 index 49c04289ddcb..000000000000 --- a/.changeset/importer-progress-bar.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed Importer Progress Bar progress indicator - diff --git a/.changeset/khaki-feet-dance.md b/.changeset/khaki-feet-dance.md new file mode 100644 index 000000000000..a419afa34143 --- /dev/null +++ b/.changeset/khaki-feet-dance.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +feat: Save visitor's activity on agent's interaction diff --git a/.changeset/kind-books-love.md b/.changeset/kind-books-love.md new file mode 100644 index 000000000000..40ce15453ff4 --- /dev/null +++ b/.changeset/kind-books-love.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed message disappearing from room after erased even if "Show Deleted Status" is enabled diff --git a/.changeset/kind-students-worry.md b/.changeset/kind-students-worry.md deleted file mode 100644 index 554c1c1204ea..000000000000 --- a/.changeset/kind-students-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Make user default role setting public diff --git a/.changeset/large-pandas-beam.md b/.changeset/large-pandas-beam.md new file mode 100644 index 000000000000..19f1eade9a9b --- /dev/null +++ b/.changeset/large-pandas-beam.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +New setting to automatically enable autotranslate when joining rooms diff --git a/.changeset/lazy-ghosts-design.md b/.changeset/lazy-ghosts-design.md deleted file mode 100644 index 080e9986cebb..000000000000 --- a/.changeset/lazy-ghosts-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed misleading of 'total' in team members list inside Channel diff --git a/.changeset/loud-bees-smoke.md b/.changeset/loud-bees-smoke.md deleted file mode 100644 index 7b34a0d58af4..000000000000 --- a/.changeset/loud-bees-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -New helper for Apps to notify users via a Direct Message diff --git a/.changeset/loud-sheep-try.md b/.changeset/loud-sheep-try.md deleted file mode 100644 index f82d0d069554..000000000000 --- a/.changeset/loud-sheep-try.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": minor -"@rocket.chat/uikit-playground": minor ---- - -feat: Adding new UIKit components: Callout, Checkbox, Radio Button, Time Picker, Toast Bar, Toggle Switch, Tab Navigation diff --git a/.changeset/lovely-snails-drop.md b/.changeset/lovely-snails-drop.md deleted file mode 100644 index 4e28c6a43c20..000000000000 --- a/.changeset/lovely-snails-drop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -Fix spotlight search does not find rooms with special or non-latin characters diff --git a/.changeset/lucky-balloons-divide.md b/.changeset/lucky-balloons-divide.md deleted file mode 100644 index beb4cbfe3b57..000000000000 --- a/.changeset/lucky-balloons-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix engagement dashboard not showing data diff --git a/.changeset/lucky-hounds-sing.md b/.changeset/lucky-hounds-sing.md deleted file mode 100644 index 20b09afaf545..000000000000 --- a/.changeset/lucky-hounds-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed wrong user status displayed during mentioning a user in a channel diff --git a/.changeset/many-icons-provide.md b/.changeset/many-icons-provide.md deleted file mode 100644 index bf82407980ad..000000000000 --- a/.changeset/many-icons-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Don't allow to report self messages diff --git a/.changeset/mighty-walls-smash.md b/.changeset/mighty-walls-smash.md deleted file mode 100644 index 54b2846901de..000000000000 --- a/.changeset/mighty-walls-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed scrollbar over content in Federated Room List diff --git a/.changeset/moody-comics-cheat.md b/.changeset/moody-comics-cheat.md deleted file mode 100644 index b8b372306d0e..000000000000 --- a/.changeset/moody-comics-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/release-action': minor ---- - -Add back "Engine Versions" to the release notes diff --git a/.changeset/moody-pans-act.md b/.changeset/moody-pans-act.md deleted file mode 100644 index 6c307604eaa9..000000000000 --- a/.changeset/moody-pans-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix seat counter including bots users diff --git a/.changeset/nice-chairs-add.md b/.changeset/nice-chairs-add.md new file mode 100644 index 000000000000..dfc9d763e1c0 --- /dev/null +++ b/.changeset/nice-chairs-add.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +--- + +Added `push` statistic, containing three bits. Each bit represents a boolean: +``` +1 1 1 +| | | +| | +- push enabled = 0b1 = 1 +| +--- push gateway enabled = 0b10 = 2 ++----- push gateway changed = 0b100 = 4 +``` diff --git a/.changeset/nine-carrots-listen.md b/.changeset/nine-carrots-listen.md deleted file mode 100644 index bf5dc72e6cc0..000000000000 --- a/.changeset/nine-carrots-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed layout changing from embedded view when navigating diff --git a/.changeset/odd-hounds-thank.md b/.changeset/odd-hounds-thank.md new file mode 100644 index 000000000000..aaddc5d51a38 --- /dev/null +++ b/.changeset/odd-hounds-thank.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: Change plan name Enterprise to Premium on marketplace diff --git a/.changeset/old-federation-card.md b/.changeset/old-federation-card.md deleted file mode 100644 index fa9879d84426..000000000000 --- a/.changeset/old-federation-card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Removed old/deprecated Rocket.Chat Federation card from Info page diff --git a/.changeset/perfect-adults-travel.md b/.changeset/perfect-adults-travel.md deleted file mode 100644 index 61ae4ab6dad5..000000000000 --- a/.changeset/perfect-adults-travel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": patch -"@rocket.chat/uikit-playground": patch ---- - -feat(fuselage-ui-kit): Introduce `TabsNavigationBlock` diff --git a/.changeset/pink-zoos-join.md b/.changeset/pink-zoos-join.md deleted file mode 100644 index dcc1088de0b5..000000000000 --- a/.changeset/pink-zoos-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix the code that was setting email URL to an invalid value when SMTP was not set diff --git a/.changeset/pretty-bees-give.md b/.changeset/pretty-bees-give.md deleted file mode 100644 index 8891420308c5..000000000000 --- a/.changeset/pretty-bees-give.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor ---- - -Add option to select what URL previews should be generated for each message. diff --git a/.changeset/proud-shrimps-cheat.md b/.changeset/proud-shrimps-cheat.md new file mode 100644 index 000000000000..cad8bc8bfa32 --- /dev/null +++ b/.changeset/proud-shrimps-cheat.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Unable to send attachments via email as an omni-agent diff --git a/.changeset/quick-emus-march.md b/.changeset/quick-emus-march.md deleted file mode 100644 index 7a6d7b444654..000000000000 --- a/.changeset/quick-emus-march.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/ddp-client': minor -'@rocket.chat/core-services': minor -'@rocket.chat/meteor': minor ---- - -Add new event to notify users directly about new banners diff --git a/.changeset/quiet-phones-reply.md b/.changeset/quiet-phones-reply.md new file mode 100644 index 000000000000..f2735e615491 --- /dev/null +++ b/.changeset/quiet-phones-reply.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Search users using full name too on share message modal diff --git a/.changeset/quiet-phones-sell.md b/.changeset/quiet-phones-sell.md deleted file mode 100644 index a6222cba16c9..000000000000 --- a/.changeset/quiet-phones-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue where oauth login was not working with some providers diff --git a/.changeset/rare-sheep-yawn.md b/.changeset/rare-sheep-yawn.md deleted file mode 100644 index 86c2d7283223..000000000000 --- a/.changeset/rare-sheep-yawn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -fix: Issue caused by spaces in the `config.url` setting diff --git a/.changeset/real-pets-visit.md b/.changeset/real-pets-visit.md deleted file mode 100644 index d6531285597c..000000000000 --- a/.changeset/real-pets-visit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch ---- - -Fixed `default` field not being returned from the `setDefault` endpoints when setting to false diff --git a/.changeset/red-windows-admire.md b/.changeset/red-windows-admire.md deleted file mode 100644 index 48a82b5902cb..000000000000 --- a/.changeset/red-windows-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed an issue where timeout for http requests in Apps-Engine bridges was too short diff --git a/.changeset/rotten-turtles-agree.md b/.changeset/rotten-turtles-agree.md deleted file mode 100644 index f915aa38f758..000000000000 --- a/.changeset/rotten-turtles-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: stop blinking "Room not found" before dm creation diff --git a/.changeset/selfish-hounds-pay.md b/.changeset/selfish-hounds-pay.md new file mode 100644 index 000000000000..3ca321bd392f --- /dev/null +++ b/.changeset/selfish-hounds-pay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Monitors now able to forward a chat without taking it first diff --git a/.changeset/serious-garlics-clean.md b/.changeset/serious-garlics-clean.md deleted file mode 100644 index ccdc3c94dda4..000000000000 --- a/.changeset/serious-garlics-clean.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/core-services': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -New AddUser workflow for Federated Rooms diff --git a/.changeset/serious-shrimps-try.md b/.changeset/serious-shrimps-try.md deleted file mode 100644 index 114293aa104e..000000000000 --- a/.changeset/serious-shrimps-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed an issue with the positioning of the message menu diff --git a/.changeset/seven-carpets-march.md b/.changeset/seven-carpets-march.md new file mode 100644 index 000000000000..46fd1b7ddb62 --- /dev/null +++ b/.changeset/seven-carpets-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Add new permission to allow kick users from rooms without being a member diff --git a/.changeset/seven-jobs-tickle.md b/.changeset/seven-jobs-tickle.md deleted file mode 100644 index 870bafbb7d9d..000000000000 --- a/.changeset/seven-jobs-tickle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -fix: agent role being removed upon user deactivation diff --git a/.changeset/shaggy-beans-poke.md b/.changeset/shaggy-beans-poke.md deleted file mode 100644 index 31a480638952..000000000000 --- a/.changeset/shaggy-beans-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix `mention-here` and `mention-all` permissions not being honored diff --git a/.changeset/shiny-garlics-carry.md b/.changeset/shiny-garlics-carry.md deleted file mode 100644 index 117063d93f6f..000000000000 --- a/.changeset/shiny-garlics-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix CORS headers not being set for assets diff --git a/.changeset/odd-elephants-promise.md b/.changeset/shiny-pillows-run.md similarity index 55% rename from .changeset/odd-elephants-promise.md rename to .changeset/shiny-pillows-run.md index a12817ed175b..9a85d37a2f9d 100644 --- a/.changeset/odd-elephants-promise.md +++ b/.changeset/shiny-pillows-run.md @@ -2,4 +2,4 @@ "@rocket.chat/meteor": patch --- -Fix LinkedIn OAuth broken +fix: cloud alerts not working diff --git a/.changeset/shiny-tools-worry.md b/.changeset/shiny-tools-worry.md deleted file mode 100644 index f024eca38d04..000000000000 --- a/.changeset/shiny-tools-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -feat: remove enforce password fallback dependency diff --git a/.changeset/short-cobras-tell.md b/.changeset/short-cobras-tell.md deleted file mode 100644 index 1c28ce7bad11..000000000000 --- a/.changeset/short-cobras-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Reorganized the message menu diff --git a/.changeset/silly-actors-laugh.md b/.changeset/silly-actors-laugh.md deleted file mode 100644 index aab23e14e5f1..000000000000 --- a/.changeset/silly-actors-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed Slackbridge was not handling correctly received events from Slack anymore. Events: (Send, edit, delete, react meassages) diff --git a/.changeset/silver-mugs-unite.md b/.changeset/silver-mugs-unite.md deleted file mode 100644 index be74b1bef215..000000000000 --- a/.changeset/silver-mugs-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: show requested filters only on requested apps view diff --git a/.changeset/six-buckets-eat.md b/.changeset/six-buckets-eat.md deleted file mode 100644 index f99bcfb71c30..000000000000 --- a/.changeset/six-buckets-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix users not able to login after block time perdiod has passed diff --git a/.changeset/slimy-wasps-double.md b/.changeset/slimy-wasps-double.md deleted file mode 100644 index b28de342b274..000000000000 --- a/.changeset/slimy-wasps-double.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/meteor': minor -'@rocket.chat/ui-contexts': minor ---- - -UX improvement for the Moderation Console Context bar for viewing the reported messages. The Report reason is now displayed in the reported messages context bar. -The Moderation Action Modal confirmation description is updated to be more clear and concise. diff --git a/.changeset/slow-lizards-breathe.md b/.changeset/slow-lizards-breathe.md deleted file mode 100644 index fd773b17f5c8..000000000000 --- a/.changeset/slow-lizards-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed Apps-Engine event `IPostUserCreated` execution diff --git a/.changeset/small-rice-repair.md b/.changeset/small-rice-repair.md deleted file mode 100644 index 67fdff5ca758..000000000000 --- a/.changeset/small-rice-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix validation in app status call that allowed Enterprise apps to be enabled in invalid environments diff --git a/.changeset/smooth-planes-cough.md b/.changeset/smooth-planes-cough.md deleted file mode 100644 index 9ad4239f0342..000000000000 --- a/.changeset/smooth-planes-cough.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/ui-theming': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -feat: high-contrast theme diff --git a/.changeset/soft-cows-juggle.md b/.changeset/soft-cows-juggle.md new file mode 100644 index 000000000000..6fcb20506483 --- /dev/null +++ b/.changeset/soft-cows-juggle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +download translation files through CDN diff --git a/.changeset/soft-yaks-matter.md b/.changeset/soft-yaks-matter.md deleted file mode 100644 index c326eb7dca70..000000000000 --- a/.changeset/soft-yaks-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -feat: return all broken password policies at once diff --git a/.changeset/sour-cows-refuse.md b/.changeset/sour-cows-refuse.md deleted file mode 100644 index d907c063f568..000000000000 --- a/.changeset/sour-cows-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed inviter not informed when inviting member to room via `/invite` slashcommand diff --git a/.changeset/sour-parrots-nail.md b/.changeset/sour-parrots-nail.md deleted file mode 100644 index 1c1eaa3173a8..000000000000 --- a/.changeset/sour-parrots-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed "teams" icon not being displayed on spotlight sidebar search diff --git a/.changeset/stale-roses-knock.md b/.changeset/stale-roses-knock.md deleted file mode 100644 index 25e93fa8c346..000000000000 --- a/.changeset/stale-roses-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: User timezone not being respected on Current Chat's filter diff --git a/.changeset/strange-papayas-yell.md b/.changeset/strange-papayas-yell.md new file mode 100644 index 000000000000..ca194dd2f9d4 --- /dev/null +++ b/.changeset/strange-papayas-yell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Disables GenericMenu without any sections or items diff --git a/.changeset/strong-laws-pump.md b/.changeset/strong-laws-pump.md deleted file mode 100644 index a4afefd65316..000000000000 --- a/.changeset/strong-laws-pump.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@rocket.chat/model-typings': patch -'@rocket.chat/meteor': patch ---- - -Change SAU aggregation to consider only sessions from few days ago instead of the whole past. - -This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. diff --git a/.changeset/sweet-chefs-exist.md b/.changeset/sweet-chefs-exist.md new file mode 100644 index 000000000000..6ceee63dd762 --- /dev/null +++ b/.changeset/sweet-chefs-exist.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Check for room scoped permissions for starting discussions diff --git a/.changeset/sweet-feet-relate.md b/.changeset/sweet-feet-relate.md new file mode 100644 index 000000000000..f7da740ebcc0 --- /dev/null +++ b/.changeset/sweet-feet-relate.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: user dropdown menu position on RTL layout diff --git a/.changeset/swift-birds-build.md b/.changeset/swift-birds-build.md deleted file mode 100644 index 4af3bddd875b..000000000000 --- a/.changeset/swift-birds-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed unable to create admin user using ADMIN\_\* environment variables diff --git a/.changeset/swift-walls-protect.md b/.changeset/swift-walls-protect.md deleted file mode 100644 index 6e3057775c32..000000000000 --- a/.changeset/swift-walls-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed failing user data exports diff --git a/.changeset/tame-pens-occur.md b/.changeset/tame-pens-occur.md deleted file mode 100644 index 8cb729531fae..000000000000 --- a/.changeset/tame-pens-occur.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/model-typings": minor ---- - -Fixed read receipts not getting deleted after corresponding message is deleted \ No newline at end of file diff --git a/.changeset/thirty-jokes-compete.md b/.changeset/thirty-jokes-compete.md new file mode 100644 index 000000000000..9d4095e7771b --- /dev/null +++ b/.changeset/thirty-jokes-compete.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: Deprecate un-used meteor method for omnichannel analytics diff --git a/.changeset/serious-geckos-drive.md b/.changeset/thirty-pumpkins-fix.md similarity index 50% rename from .changeset/serious-geckos-drive.md rename to .changeset/thirty-pumpkins-fix.md index 454337399772..11b92b064e15 100644 --- a/.changeset/serious-geckos-drive.md +++ b/.changeset/thirty-pumpkins-fix.md @@ -1,8 +1,8 @@ --- '@rocket.chat/core-typings': minor '@rocket.chat/rest-typings': minor -'@rocket.chat/ui-client': minor +'@rocket.chat/tools': minor '@rocket.chat/meteor': minor --- -Added Reports Metrics Dashboard to Omnichannel +Added option to select between two script engine options for the integrations diff --git a/.changeset/three-birds-tickle.md b/.changeset/three-birds-tickle.md deleted file mode 100644 index 0ce911d9f6fa..000000000000 --- a/.changeset/three-birds-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. diff --git a/.changeset/tidy-bears-applaud.md b/.changeset/tidy-bears-applaud.md new file mode 100644 index 000000000000..cff12f3dc7d3 --- /dev/null +++ b/.changeset/tidy-bears-applaud.md @@ -0,0 +1,10 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor +--- + +Create a deployment fingerprint to identify possible deployment changes caused by database cloning. A question to the admin will confirm if it's a regular deployment change or an intent of a new deployment and correct identification values as needed. +The fingerprint is composed by `${siteUrl}${dbConnectionString}` and hashed via `sha256` in `base64`. +An environment variable named `AUTO_ACCEPT_FINGERPRINT`, when set to `true`, can be used to auto-accept an expected fingerprint change as a regular deployment update. diff --git a/.changeset/tidy-bears-camp.md b/.changeset/tidy-bears-camp.md deleted file mode 100644 index 3c2013f79023..000000000000 --- a/.changeset/tidy-bears-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Introduced upsells for the engagement dashboard and device management admin sidebar items in CE workspaces. Additionally, restructured the admin sidebar items to enhance organization. diff --git a/.changeset/tiny-turkeys-burn.md b/.changeset/tiny-turkeys-burn.md deleted file mode 100644 index a146bd6a0eae..000000000000 --- a/.changeset/tiny-turkeys-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue on oauth login that caused missing emails to be detected as changed data diff --git a/.changeset/tiny-wolves-deliver.md b/.changeset/tiny-wolves-deliver.md new file mode 100644 index 000000000000..f89564a9b53c --- /dev/null +++ b/.changeset/tiny-wolves-deliver.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/ui-theming': patch +--- + +fix: light-theme font-disabled color diff --git a/.changeset/tough-candles-heal.md b/.changeset/tough-candles-heal.md deleted file mode 100644 index 59ad9c1fb3a1..000000000000 --- a/.changeset/tough-candles-heal.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch -"@rocket.chat/model-typings": patch ---- - -Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. diff --git a/.changeset/tough-carrots-walk.md b/.changeset/tough-carrots-walk.md new file mode 100644 index 000000000000..2851e697b85e --- /dev/null +++ b/.changeset/tough-carrots-walk.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/license': patch +'@rocket.chat/meteor': patch +--- + +feat: added `licenses.info` endpoint diff --git a/.changeset/tricky-years-swim.md b/.changeset/tricky-years-swim.md deleted file mode 100644 index 2ab1254525b2..000000000000 --- a/.changeset/tricky-years-swim.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/rest-typings": patch ---- - -Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data diff --git a/.changeset/twelve-files-deny.md b/.changeset/twelve-files-deny.md new file mode 100644 index 000000000000..123bf0a7764b --- /dev/null +++ b/.changeset/twelve-files-deny.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/license': minor +'@rocket.chat/jwt': minor +'@rocket.chat/omnichannel-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/account-service': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/meteor': minor +--- + +Implemented the License library, it is used to handle the functionality like expiration date, modules, limits, etc. +Also added a version v3 of the license, which contains an extended list of features. +v2 is still supported, since we convert it to v3 on the fly. diff --git a/.changeset/unlucky-turtles-search.md b/.changeset/unlucky-turtles-search.md deleted file mode 100644 index fffa51020e30..000000000000 --- a/.changeset/unlucky-turtles-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed Accounts profile form name change was not working diff --git a/.changeset/user-mention.md b/.changeset/user-mention.md deleted file mode 100644 index a896a7c12ee4..000000000000 --- a/.changeset/user-mention.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed user mentioning when prepending the username with `>` diff --git a/.changeset/violet-frogs-cheer.md b/.changeset/violet-frogs-cheer.md deleted file mode 100644 index db48243c40ed..000000000000 --- a/.changeset/violet-frogs-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/fuselage-ui-kit': patch ---- - -Handle invalid context on `VideoConferenceBlock` component diff --git a/.changeset/warm-hornets-ring.md b/.changeset/warm-hornets-ring.md deleted file mode 100644 index f81cf1efbe92..000000000000 --- a/.changeset/warm-hornets-ring.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch ---- - -Use group filter when set to LDAP sync process diff --git a/.changeset/warm-melons-type.md b/.changeset/warm-melons-type.md new file mode 100644 index 000000000000..5b187b8a7f11 --- /dev/null +++ b/.changeset/warm-melons-type.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/omnichannel-services": patch +--- + +feat: Disable and annonimize visitors instead of removing diff --git a/.changeset/wet-frogs-kiss.md b/.changeset/wet-frogs-kiss.md deleted file mode 100644 index 24395a78f85d..000000000000 --- a/.changeset/wet-frogs-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Missing padding on Omnichannel contacts Contextualbar loading state diff --git a/.changeset/wet-walls-lie.md b/.changeset/wet-walls-lie.md deleted file mode 100644 index 6b18eb497686..000000000000 --- a/.changeset/wet-walls-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes a problem that allowed users to send empty spaces as comment to bypass the "comment required" setting diff --git a/.changeset/red-zebras-clap.md b/.changeset/wicked-humans-hang.md similarity index 53% rename from .changeset/red-zebras-clap.md rename to .changeset/wicked-humans-hang.md index cd8f832b1835..e793bc978902 100644 --- a/.changeset/red-zebras-clap.md +++ b/.changeset/wicked-humans-hang.md @@ -2,4 +2,4 @@ "@rocket.chat/meteor": patch --- -Fix importer filters not working +Improve cache of static files diff --git a/.changeset/wild-spiders-smell.md b/.changeset/wild-spiders-smell.md deleted file mode 100644 index 9694d6259d3a..000000000000 --- a/.changeset/wild-spiders-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where a mailer error was being sent to customers using offline message's form on Omnichannel instead of the translated one diff --git a/.changeset/wise-onions-trade.md b/.changeset/wise-onions-trade.md deleted file mode 100644 index cb5c731fb6fb..000000000000 --- a/.changeset/wise-onions-trade.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch -"@rocket.chat/livechat": patch -"@rocket.chat/mock-providers": patch -"@rocket.chat/ui-client": patch -"@rocket.chat/ui-contexts": patch -"@rocket.chat/web-ui-registration": patch ---- - -Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. diff --git a/.changeset/wise-walls-tan.md b/.changeset/wise-walls-tan.md deleted file mode 100644 index f558de82ec4c..000000000000 --- a/.changeset/wise-walls-tan.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -fix: missing params on updateOwnBasicInfo endpoint diff --git a/.changeset/wise-ways-fetch.md b/.changeset/wise-ways-fetch.md deleted file mode 100644 index a81063813c35..000000000000 --- a/.changeset/wise-ways-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed the unread messages mark not showing diff --git a/.changeset/witty-feet-warn.md b/.changeset/witty-feet-warn.md deleted file mode 100644 index faaa5d44c134..000000000000 --- a/.changeset/witty-feet-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed the video recorder window not closing after permission is denied. diff --git a/.changeset/yellow-buttons-agree.md b/.changeset/yellow-buttons-agree.md deleted file mode 100644 index a86d172a4544..000000000000 --- a/.changeset/yellow-buttons-agree.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/ui-client': minor -'@rocket.chat/meteor': minor ---- - -feat: add ChangePassword field to Account/Security diff --git a/.changeset/yellow-schools-tell.md b/.changeset/yellow-schools-tell.md deleted file mode 100644 index c1040fa0856a..000000000000 --- a/.changeset/yellow-schools-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/eslint-config': minor ---- - -Unpublished changes in ESLint config diff --git a/.changeset/young-trains-glow.md b/.changeset/young-trains-glow.md deleted file mode 100644 index 77f50812143f..000000000000 --- a/.changeset/young-trains-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Fixed the issue of apps icon uneven alignment in case of missing icons inside message composer toolbar & message toolbar menu. diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 808b8acdcbe3..284a0985b78e 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -19,6 +19,7 @@ runs: steps: - name: Login to GitHub Container Registry + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') uses: docker/login-action@v2 with: registry: ghcr.io @@ -62,6 +63,7 @@ runs: docker compose -f docker-compose-ci.yml build "${args[@]}" - name: Publish Docker images to GitHub Container Registry + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') shell: bash run: | args=(rocketchat) diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 5a556a1a8e29..57cdac047423 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -44,6 +44,17 @@ jobs: - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + - name: Cache TypeCheck + uses: actions/cache@v3 + if: matrix.check == 'ts' + with: + path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo + key: typecheck-cache-${{ runner.OS }}-${{ hashFiles('yarn.lock') }}-${{ github.event.issue.number }} + restore-keys: | + typecheck-cache-${{ runner.OS }}-${{ hashFiles('yarn.lock') }} + typecheck-cache-${{ runner.OS }} + typecheck-cache + - name: TS TypeCheck if: matrix.check == 'ts' run: yarn turbo run typecheck diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e14857a97a09..d77966f186b3 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -97,6 +97,14 @@ jobs: cache-modules: true install: true + # 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 + with: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + node-version: ${{ inputs.node-version }} + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - name: Start httpbin container and wait for it to be ready diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22250705ea58..ec8e905cd803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,7 +172,6 @@ jobs: build-gh-docker-coverage: name: 🚢 Build Docker Images for Testing needs: [build, release-versions] - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') runs-on: ubuntu-20.04 env: @@ -189,7 +188,10 @@ jobs: steps: - uses: actions/checkout@v3 + + # we only build and publish the actual docker images if not a PR from a fork - uses: ./.github/actions/build-docker + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} @@ -356,13 +358,16 @@ jobs: echo finished deploy: - name: 🚀 Publish build and update our registry + name: 🚀 Publish build assets runs-on: ubuntu-20.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' needs: [build-gh-docker, release-versions] steps: - - uses: actions/checkout@v3 + - uses: Bhacaz/checkout-files@v2 + with: + files: package.json + branch: ${{ github.ref }} - name: Restore build uses: actions/download-artifact@v3 @@ -376,32 +381,17 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: 'us-east-1' GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} - REDHAT_REGISTRY_PID: ${{ secrets.REDHAT_REGISTRY_PID }} - REDHAT_REGISTRY_KEY: ${{ secrets.REDHAT_REGISTRY_KEY }} - UPDATE_TOKEN: ${{ secrets.UPDATE_TOKEN }} run: | REPO_VERSION=$(node -p "require('./package.json').version") + if [[ '${{ github.event_name }}' = 'release' ]]; then GIT_TAG="${GITHUB_REF#*tags/}" - GIT_BRANCH="" ARTIFACT_NAME="${REPO_VERSION}" - RC_VERSION=$GIT_TAG - - if [[ '${{ needs.release-versions.outputs.release }}' = 'release-candidate' ]]; then - SNAP_CHANNEL=candidate - RC_RELEASE=candidate - elif [[ '${{ needs.release-versions.outputs.release }}' = 'latest' ]]; then - SNAP_CHANNEL=stable - RC_RELEASE=stable - fi else GIT_TAG="" - GIT_BRANCH="${GITHUB_REF#*heads/}" ARTIFACT_NAME="${REPO_VERSION}.$GITHUB_SHA" - RC_VERSION="${REPO_VERSION}" - SNAP_CHANNEL=edge - RC_RELEASE=develop fi; + ROCKET_DEPLOY_DIR="/tmp/deploy" FILENAME="$ROCKET_DEPLOY_DIR/rocket.chat-$ARTIFACT_NAME.tgz"; @@ -419,22 +409,6 @@ jobs: aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive - curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"compatibleMongoVersions\": [\"4.4\", \"5.0\", \"6.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ - https://releases.rocket.chat/update - - # Makes build fail if the release isn't there - curl --fail https://releases.rocket.chat/$RC_VERSION/info - - if [[ $GIT_TAG ]]; then - curl -X POST \ - https://connect.redhat.com/api/v2/projects/$REDHAT_REGISTRY_PID/build \ - -H "Authorization: Bearer $REDHAT_REGISTRY_KEY" \ - -H 'Cache-Control: no-cache' \ - -H 'Content-Type: application/json' \ - -d '{"tag":"'$GIT_TAG'"}' - fi - build-docker-preview: name: 🚢 Build Docker Image (preview) runs-on: ubuntu-20.04 @@ -665,6 +639,66 @@ jobs: echo "::endgroup::" + notify-services: + name: 🚀 Notify external services + runs-on: ubuntu-20.04 + needs: + - services-docker-image-publish + - docker-image-publish + - release-versions + steps: + - uses: Bhacaz/checkout-files@v2 + with: + files: package.json + branch: ${{ github.ref }} + + - name: Releases service + env: + UPDATE_TOKEN: ${{ secrets.UPDATE_TOKEN }} + run: | + REPO_VERSION=$(node -p "require('./package.json').version") + + if [[ '${{ github.event_name }}' = 'release' ]]; then + GIT_TAG="${GITHUB_REF#*tags/}" + GIT_BRANCH="" + ARTIFACT_NAME="${REPO_VERSION}" + RC_VERSION=$GIT_TAG + + if [[ '${{ needs.release-versions.outputs.release }}' = 'release-candidate' ]]; then + RC_RELEASE=candidate + elif [[ '${{ needs.release-versions.outputs.release }}' = 'latest' ]]; then + RC_RELEASE=stable + fi + else + GIT_TAG="" + GIT_BRANCH="${GITHUB_REF#*heads/}" + ARTIFACT_NAME="${REPO_VERSION}.$GITHUB_SHA" + RC_VERSION="${REPO_VERSION}" + RC_RELEASE=develop + fi; + + curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ + "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"compatibleMongoVersions\": [\"4.4\", \"5.0\", \"6.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ + https://releases.rocket.chat/update + + # Makes build fail if the release isn't there + curl --fail https://releases.rocket.chat/$RC_VERSION/info + + - name: RedHat Registry + if: github.event_name == 'release' + env: + REDHAT_REGISTRY_PID: ${{ secrets.REDHAT_REGISTRY_PID }} + REDHAT_REGISTRY_KEY: ${{ secrets.REDHAT_REGISTRY_KEY }} + run: | + GIT_TAG="${GITHUB_REF#*tags/}" + + curl -X POST \ + https://connect.redhat.com/api/v2/projects/$REDHAT_REGISTRY_PID/build \ + -H "Authorization: Bearer $REDHAT_REGISTRY_KEY" \ + -H 'Cache-Control: no-cache' \ + -H 'Content-Type: application/json' \ + -d '{"tag":"'$GIT_TAG'"}' + trigger-dependent-workflows: runs-on: ubuntu-latest if: github.event_name == 'release' diff --git a/.github/workflows/vulnerabilities-jira-integration.yml b/.github/workflows/vulnerabilities-jira-integration.yml new file mode 100644 index 000000000000..2daeb533937d --- /dev/null +++ b/.github/workflows/vulnerabilities-jira-integration.yml @@ -0,0 +1,22 @@ +name: Github vulnerabilities and jira board integration + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + IntegrateSecurityVulnerabilities: + runs-on: ubuntu-latest + steps: + - name: "Github vulnerabilities and jira board integration" + uses: RocketChat/github-vulnerabilities-jira-integration@v0.3 + env: + JIRA_URL: https://rocketchat.atlassian.net/ + JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} + GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + JIRA_EMAIL: security-team-accounts@rocket.chat + JIRA_PROJECT_ID: GJIT + UID_CUSTOMFIELD_ID: customfield_10059 + JIRA_COMPLETE_PHASE_ID: 31 + JIRA_START_PHASE_ID: 11 + diff --git a/.yarn/patches/mongodb-npm-4.17.1-a2fe811ff1.patch b/.yarn/patches/mongodb-npm-4.17.1-a2fe811ff1.patch new file mode 100644 index 000000000000..501881370244 --- /dev/null +++ b/.yarn/patches/mongodb-npm-4.17.1-a2fe811ff1.patch @@ -0,0 +1,13 @@ +diff --git a/mongodb.d.ts b/mongodb.d.ts +index dd080b553309594c28093365ea101adec5c0a20c..20a616de8c97ec68629c01a848ea8df4fe122bf2 100644 +--- a/mongodb.d.ts ++++ b/mongodb.d.ts +@@ -5539,7 +5539,7 @@ export declare interface MonitorOptions extends Omit = Depth['length'] extends 8 ? [] : Type extends string | number | boolean | Date | RegExp | Buffer | Uint8Array | ((...args: any[]) => any) | { ++export declare type NestedPaths = Depth['length'] extends 1 ? [] : Type extends string | number | boolean | Date | RegExp | Buffer | Uint8Array | ((...args: any[]) => any) | { + _bsontype: string; + } ? [] : Type extends ReadonlyArray ? [] | [number, ...NestedPaths] : Type extends Map ? [string] : Type extends object ? { + [Key in Extract]: Type[Key] extends Type ? [Key] : Type extends Type[Key] ? [Key] : Type[Key] extends ReadonlyArray ? Type extends ArrayType ? [Key] : ArrayType extends Type ? [Key] : [ diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 62a0476d9077..003baa57aa8b 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -15,6 +15,11 @@ RUN set -x \ && npm install sharp@0.30.4 \ && mv node_modules/sharp npm/node_modules/sharp \ # End hack for sharp + # Start hack for isolated-vm... + && rm -rf npm/node_modules/isolated-vm \ + && npm install isolated-vm@4.4.2 \ + && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ + # End hack for isolated-vm && cd npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 9f03ae9f3db4..1481b0445e45 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 6.4.0-develop +ENV RC_VERSION 6.5.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/.gitignore b/apps/meteor/.gitignore index 287cb313c174..a9fd54ab8711 100644 --- a/apps/meteor/.gitignore +++ b/apps/meteor/.gitignore @@ -87,4 +87,5 @@ out.txt dist *-session.json matrix-federation-config/* -.eslintcache \ No newline at end of file +.eslintcache +tsconfig.typecheck.tsbuildinfo diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 97836558ab28..ae788af78034 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -25,7 +25,7 @@ accounts-password@2.3.4 accounts-twitter@1.5.0 pauli:accounts-linkedin -google-oauth@1.4.3 +google-oauth@1.4.4 oauth@2.2.0 oauth2@1.3.2 @@ -39,7 +39,7 @@ meteor-base@1.5.1 ddp-common@1.4.0 webapp@1.13.5 -mongo@1.16.6 +mongo@1.16.7 reload@1.3.1 service-configuration@1.3.1 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index e8cfc7ec4c01..6641d0478a10 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.12 +METEOR@2.13.3 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index da6de9efbde1..66f61e2cd8cc 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -22,7 +22,7 @@ ddp@1.4.1 ddp-client@2.6.1 ddp-common@1.4.0 ddp-rate-limiter@1.2.0 -ddp-server@2.6.1 +ddp-server@2.6.2 diff-sequence@1.1.2 dispatch:run-as-user@1.1.1 dynamic-import@0.7.3 @@ -38,7 +38,7 @@ facts-base@1.0.1 fetch@0.1.3 geojson-utils@1.0.11 github-oauth@1.4.1 -google-oauth@1.4.3 +google-oauth@1.4.4 hot-code-push@1.0.4 http@2.0.0 id-map@1.1.1 @@ -47,7 +47,7 @@ jquery@3.0.0 kadira:flow-router@2.12.1 localstorage@1.2.0 logging@1.3.2 -meteor@1.11.2 +meteor@1.11.3 meteor-base@1.5.1 meteor-developer-oauth@1.3.2 meteorhacks:inject-initial@1.0.5 @@ -56,7 +56,7 @@ minimongo@1.9.3 modern-browsers@0.1.9 modules@0.19.0 modules-runtime@0.13.1 -mongo@1.16.6 +mongo@1.16.7 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 @@ -92,7 +92,7 @@ shell-server@0.5.0 socket-stream-client@0.5.1 standard-minifier-css@1.9.2 tracker@1.3.2 -twitter-oauth@1.3.2 +twitter-oauth@1.3.3 typescript@4.9.4 underscore@1.0.13 url@1.3.2 diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 51ff6d4d1826..9c2e0b63e240 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,597 @@ # @rocket.chat/meteor +## 6.4.0 + +### Minor Changes + +- 239a34e877: new: ring mobile users on direct conference calls +- 04fe492555: Added new Omnichannel's trigger condition "After starting a chat". +- 4186eecf05: Introduce the ability to report an user +- 92b690d206: fix: Wrong toast message while creating a new custom sound with an existing name +- f83ea5d6e8: Added support for threaded conversation in Federated rooms. +- 682d0bc05a: fix: Time format of Retention Policy +- 1b42dfc6c1: Added a new Roles bridge to RC Apps-Engine for reading and retrieving role details. +- 2db32f0d4a: Add option to select what URL previews should be generated for each message. +- 982ef6f459: Add new event to notify users directly about new banners +- 19aec23cda: New AddUser workflow for Federated Rooms +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- 85a936220c: feat: remove enforce password fallback dependency +- 5832be2e1b: Reorganized the message menu +- 074db3b419: UX improvement for the Moderation Console Context bar for viewing the reported messages. The Report reason is now displayed in the reported messages context bar. + The Moderation Action Modal confirmation description is updated to be more clear and concise. +- 357a3a50fa: feat: high-contrast theme +- 7070f00b05: feat: return all broken password policies at once +- ead7c7bef2: Fixed read receipts not getting deleted after corresponding message is deleted +- 1041d4d361: Added option to select between two script engine options for the integrations +- ad08c26b46: Introduced upsells for the engagement dashboard and device management admin sidebar items in CE workspaces. Additionally, restructured the admin sidebar items to enhance organization. +- 93d4912e17: fix: missing params on updateOwnBasicInfo endpoint +- ee3815fce4: feat: add ChangePassword field to Account/Security +- 1000b9b317: Fixed the issue of apps icon uneven alignment in case of missing icons inside message composer toolbar & message toolbar menu. + +### Patch Changes + +- 6d453f71ac: Translation files are requested multiple times +- cada29b6ce: fix: Managers allowed to make deactivated agent's available +- 470c29d7e9: Fixed an issue causing `queue time` to be calculated from current time when a room was closed without being served. + Now: + - For served rooms: queue time = servedBy time - queuedAt + - For not served, but open rooms = now - queuedAt + - For not served and closed rooms = closedAt - queuedAt +- ea8998602b: fix: Performance issue on `Messages.countByType` aggregation caused by unindexed property on messages collection +- f634601d90: Bump @rocket.chat/meteor version. +- f46c1f7b70: Bump @rocket.chat/meteor version. +- 6963cc2d00: Bump @rocket.chat/meteor version. +- 7cc15ac814: Bump @rocket.chat/meteor version. +- 40c5277197: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- a08006c9f0: feat: add sections to room header and user infos menus with menuV2 +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- 9edca67b9b: feat(apps): `ActionManagerBusyState` component for apps `ui.interaction` +- ec60dbe8f5: Fixed custom translations not being displayed +- 6fa30ddcd1: Hide Reset TOTP option if 2FA is disabled +- ff7e181464: Added ability to freeze or completely disable integration scripts through envvars +- 4ce8ea89a8: fix: custom emoji upload with FileSystem method +- 87570d0fb7: New filters to the Rooms Table at `Workspace > Rooms` +- 8a59855fcf: When setting a room as read-only, do not allow previously unmuted users to send messages. +- c73f5373b8: fix: finnish translation +- f5a886a144: fixed an issue where 2fa was not working after an OAuth redirect +- 459c8574ed: Fixed issue with custom OAuth services' settings not being be fully removed +- 42644a6e44: fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times +- 9bdbc9b086: load sounds right before playing them +- 6154979119: Fix users being created without the `roles` field +- 6bcdd88531: Fixed CAS login after popup closes +- 839789c988: Fix moment timestamps language change +- f0025d4d92: Fixed message fetching method in LivechatBridge for Apps +- 9c957b9d9a: Fix pruning messages in a room results in an incorrect message counter +- 583a3149fe: fix: rejected conference calls continue to ring +- b59fd5d7fb: User information crashing for some locales +- 4349443629: Fix performance issue on Engagement Dashboard aggregation +- 614a9b8fc8: Show correct date for last day time +- 69447e1864: Added ability to disable private app installation via envvar (DISABLE_PRIVATE_APP_INSTALLATION) +- 52a1aa94eb: improve: System messages for omni-visitor abandonment feature +- 7dffec2e2f: chore: Add danger variant to apps action button menus +- f0c8867bb9: Disabled call to tags enterprise endpoint when on community license +- 5e89694bfa: Fixes SAML full name updates not being mirrored to DM rooms. +- d6f0c6afe2: Fixed Importer Progress Bar progress indicator +- 177506ea91: Make user default role setting public +- 3fb2124166: Fixed misleading of 'total' in team members list inside Channel +- 5cee21468e: Fix spotlight search does not find rooms with special or non-latin characters +- cf59c8abe3: Fix engagement dashboard not showing data +- dfb9a075b3: fixed wrong user status displayed during mentioning a user in a channel +- 1fbbb6241a: Don't allow to report self messages +- 53e0c346e2: fixed scrollbar over content in Federated Room List +- 5321e87363: Fix seat counter including bots users +- 7137a193a7: feat: Add flag to disable teams mention via troubleshoot page +- 59e6fe3d2a: fixed layout changing from embedded view when navigating +- 3245a0a318: Fix LinkedIn OAuth broken +- 45a8943ed4: Removed old/deprecated Rocket.Chat Federation card from Info page +- 6eea189ec8: Fix the code that was setting email URL to an invalid value when SMTP was not set +- f5a886a144: fixed an issue where oauth login was not working with some providers +- ba24f3c21f: Fixed `default` field not being returned from the `setDefault` endpoints when setting to false +- a79f61461d: Fixed an issue where timeout for http requests in Apps-Engine bridges was too short +- 51b988b3df: Fix importer filters not working +- 5d857f462c: fix: stop blinking "Room not found" before dm creation +- db26f8a8ee: fixed an issue with the positioning of the message menu +- aaefe865a7: fix: agent role being removed upon user deactivation +- 306a5830c3: Fix `mention-here` and `mention-all` permissions not being honored +- 761cad4382: Fix CORS headers not being set for assets +- 9e5718002a: Fixed Slackbridge was not handling correctly received events from Slack anymore. Events: (Send, edit, delete, react meassages) +- 54ef89c9a7: fix: show requested filters only on requested apps view +- 1589279b79: Fix users not able to login after block time perdiod has passed +- 880ab5689c: Fixed selected departments not being displayed due to pagination +- a81bad24e0: Fixed Apps-Engine event `IPostUserCreated` execution +- 7a4fdf41f8: Fix validation in app status call that allowed Enterprise apps to be enabled in invalid environments +- e28f8d95f0: Fixed inviter not informed when inviting member to room via `/invite` slashcommand +- d47d2021ac: Fixed "teams" icon not being displayed on spotlight sidebar search +- 93d5a5ceb8: fix: User timezone not being respected on Current Chat's filter +- f556518fa1: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + +- b747f3d3bc: Fixed unable to create admin user using ADMIN\_\* environment variables +- 2cf2643399: Fixed failing user data exports +- 61a106fbf2: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + +- ace35997a6: chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. +- f5a886a144: fixed an issue on oauth login that caused missing emails to be detected as changed data +- 61128364d6: Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. +- 9496f1eb97: Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data +- 01dec055a0: Fixed Accounts profile form name change was not working +- e4837a15ed: Fixed user mentioning when prepending the username with `>` +- d45365436e: Use group filter when set to LDAP sync process +- c536a4a237: fix: Missing padding on Omnichannel contacts Contextualbar loading state +- 87e4a4aa56: Fixes a problem that allowed users to send empty spaces as comment to bypass the "comment required" setting +- 69a5213afc: Fixed an issue where a mailer error was being sent to customers using offline message's form on Omnichannel instead of the translated one +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- 22cf158c43: fixed the unread messages mark not showing +- 72a34a02f7: fixed the video recorder window not closing after permission is denied. +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [1246a21648] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [f9a748526d] +- Updated dependencies [5cee21468e] +- Updated dependencies [dc1d8ce92e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [074db3b419] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [d9a150000d] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61a106fbf2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [dce4a829fa] +- Updated dependencies [d45365436e] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [93d4912e17] +- Updated dependencies [ee3815fce4] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/fuselage-ui-kit@2.0.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/ui-client@2.0.0 + - @rocket.chat/ui-contexts@2.0.0 + - @rocket.chat/ui-theming@0.1.0 + - @rocket.chat/presence@0.0.15 + - @rocket.chat/tools@0.1.0 + - @rocket.chat/cron@0.0.11 + - @rocket.chat/i18n@0.0.2 + - @rocket.chat/web-ui-registration@2.0.0 + - @rocket.chat/api-client@0.1.9 + - @rocket.chat/omnichannel-services@0.0.15 + - @rocket.chat/pdf-worker@0.0.15 + - @rocket.chat/gazzodown@2.0.0 + - @rocket.chat/models@0.0.15 + - @rocket.chat/ui-video-conf@2.0.0 + - @rocket.chat/base64@1.0.12 + - @rocket.chat/instance-status@0.0.15 + - @rocket.chat/random@1.2.1 + - @rocket.chat/sha256@1.0.9 + - @rocket.chat/ui-composer@0.0.1 + +## 6.4.0-rc.5 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations + +### Patch Changes + +- Bump @rocket.chat/meteor version. +- ec60dbe8f5: Fixed custom translations not being displayed +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/tools@0.1.0-rc.0 + - @rocket.chat/api-client@0.1.9-rc.5 + - @rocket.chat/omnichannel-services@0.0.15-rc.5 + - @rocket.chat/pdf-worker@0.0.15-rc.5 + - @rocket.chat/presence@0.0.15-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/cron@0.0.11-rc.5 + - @rocket.chat/gazzodown@2.0.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/ui-contexts@2.0.0-rc.5 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.5 + - @rocket.chat/ui-video-conf@2.0.0-rc.5 + - @rocket.chat/web-ui-registration@2.0.0-rc.5 + - @rocket.chat/instance-status@0.0.15-rc.5 + +## 6.4.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + - @rocket.chat/core-typings@6.4.0-rc.4 + - @rocket.chat/rest-typings@6.4.0-rc.4 + - @rocket.chat/api-client@0.1.8-rc.4 + - @rocket.chat/omnichannel-services@0.0.14-rc.4 + - @rocket.chat/pdf-worker@0.0.14-rc.4 + - @rocket.chat/presence@0.0.14-rc.4 + - @rocket.chat/core-services@0.2.0-rc.4 + - @rocket.chat/cron@0.0.10-rc.4 + - @rocket.chat/gazzodown@2.0.0-rc.4 + - @rocket.chat/model-typings@0.1.0-rc.4 + - @rocket.chat/ui-contexts@2.0.0-rc.4 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.4 + - @rocket.chat/models@0.0.14-rc.4 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.4 + - @rocket.chat/ui-video-conf@2.0.0-rc.4 + - @rocket.chat/web-ui-registration@2.0.0-rc.4 + - @rocket.chat/instance-status@0.0.14-rc.4 + +## 6.4.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. +- 614a9b8fc8: Show correct date for last day time +- 61a106fbf2: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + +- Updated dependencies [d9a150000d] +- Updated dependencies [61a106fbf2] + - @rocket.chat/presence@0.0.13-rc.3 + - @rocket.chat/cron@0.0.9-rc.3 + - @rocket.chat/core-typings@6.4.0-rc.3 + - @rocket.chat/rest-typings@6.4.0-rc.3 + - @rocket.chat/api-client@0.1.7-rc.3 + - @rocket.chat/omnichannel-services@0.0.13-rc.3 + - @rocket.chat/pdf-worker@0.0.13-rc.3 + - @rocket.chat/core-services@0.2.0-rc.3 + - @rocket.chat/gazzodown@2.0.0-rc.3 + - @rocket.chat/model-typings@0.1.0-rc.3 + - @rocket.chat/ui-contexts@2.0.0-rc.3 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.3 + - @rocket.chat/models@0.0.13-rc.3 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.3 + - @rocket.chat/ui-video-conf@2.0.0-rc.3 + - @rocket.chat/web-ui-registration@2.0.0-rc.3 + - @rocket.chat/instance-status@0.0.13-rc.3 + +## 6.4.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + - @rocket.chat/core-typings@6.4.0-rc.2 + - @rocket.chat/rest-typings@6.4.0-rc.2 + - @rocket.chat/api-client@0.1.7-rc.2 + - @rocket.chat/omnichannel-services@0.0.13-rc.2 + - @rocket.chat/pdf-worker@0.0.13-rc.2 + - @rocket.chat/presence@0.0.13-rc.2 + - @rocket.chat/core-services@0.2.0-rc.2 + - @rocket.chat/cron@0.0.9-rc.2 + - @rocket.chat/gazzodown@2.0.0-rc.2 + - @rocket.chat/model-typings@0.1.0-rc.2 + - @rocket.chat/ui-contexts@2.0.0-rc.2 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.2 + - @rocket.chat/models@0.0.13-rc.2 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.2 + - @rocket.chat/ui-video-conf@2.0.0-rc.2 + - @rocket.chat/web-ui-registration@2.0.0-rc.2 + - @rocket.chat/instance-status@0.0.13-rc.2 + +## 6.4.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + - @rocket.chat/core-typings@6.4.0-rc.1 + - @rocket.chat/rest-typings@6.4.0-rc.1 + - @rocket.chat/api-client@0.1.5-rc.1 + - @rocket.chat/omnichannel-services@0.0.11-rc.1 + - @rocket.chat/pdf-worker@0.0.11-rc.1 + - @rocket.chat/presence@0.0.11-rc.1 + - @rocket.chat/core-services@0.2.0-rc.1 + - @rocket.chat/cron@0.0.7-rc.1 + - @rocket.chat/gazzodown@2.0.0-rc.1 + - @rocket.chat/model-typings@0.1.0-rc.1 + - @rocket.chat/ui-contexts@2.0.0-rc.1 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.1 + - @rocket.chat/models@0.0.11-rc.1 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.1 + - @rocket.chat/ui-video-conf@2.0.0-rc.1 + - @rocket.chat/web-ui-registration@2.0.0-rc.1 + - @rocket.chat/instance-status@0.0.11-rc.1 + +## 6.4.0-rc.0 + +### Minor Changes + +- 239a34e877: new: ring mobile users on direct conference calls +- 04fe492555: Added new Omnichannel's trigger condition "After starting a chat". +- 4186eecf05: Introduce the ability to report an user +- 92b690d206: fix: Wrong toast message while creating a new custom sound with an existing name +- f83ea5d6e8: Added support for threaded conversation in Federated rooms. +- 682d0bc05a: fix: Time format of Retention Policy +- 1b42dfc6c1: Added a new Roles bridge to RC Apps-Engine for reading and retrieving role details. +- 2db32f0d4a: Add option to select what URL previews should be generated for each message. +- 982ef6f459: Add new event to notify users directly about new banners +- 19aec23cda: New AddUser workflow for Federated Rooms +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- 85a936220c: feat: remove enforce password fallback dependency +- 5832be2e1b: Reorganized the message menu +- 074db3b419: UX improvement for the Moderation Console Context bar for viewing the reported messages. The Report reason is now displayed in the reported messages context bar. + The Moderation Action Modal confirmation description is updated to be more clear and concise. +- 357a3a50fa: feat: high-contrast theme +- 7070f00b05: feat: return all broken password policies at once +- ead7c7bef2: Fixed read receipts not getting deleted after corresponding message is deleted +- ad08c26b46: Introduced upsells for the engagement dashboard and device management admin sidebar items in CE workspaces. Additionally, restructured the admin sidebar items to enhance organization. +- 93d4912e17: fix: missing params on updateOwnBasicInfo endpoint +- ee3815fce4: feat: add ChangePassword field to Account/Security +- 1000b9b317: Fixed the issue of apps icon uneven alignment in case of missing icons inside message composer toolbar & message toolbar menu. + +### Patch Changes + +- 6d453f71ac: Translation files are requested multiple times +- cada29b6ce: fix: Managers allowed to make deactivated agent's available +- 470c29d7e9: Fixed an issue causing `queue time` to be calculated from current time when a room was closed without being served. + Now: + - For served rooms: queue time = servedBy time - queuedAt + - For not served, but open rooms = now - queuedAt + - For not served and closed rooms = closedAt - queuedAt +- ea8998602b: fix: Performance issue on `Messages.countByType` aggregation caused by unindexed property on messages collection +- a08006c9f0: feat: add sections to room header and user infos menus with menuV2 +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- 9edca67b9b: feat(apps): `ActionManagerBusyState` component for apps `ui.interaction` +- 6fa30ddcd1: Hide Reset TOTP option if 2FA is disabled +- ff7e181464: Added ability to freeze or completely disable integration scripts through envvars +- 4ce8ea89a8: fix: custom emoji upload with FileSystem method +- 87570d0fb7: New filters to the Rooms Table at `Workspace > Rooms` +- 8a59855fcf: When setting a room as read-only, do not allow previously unmuted users to send messages. +- c73f5373b8: fix: finnish translation +- f5a886a144: fixed an issue where 2fa was not working after an OAuth redirect +- 459c8574ed: Fixed issue with custom OAuth services' settings not being be fully removed +- 42644a6e44: fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times +- 9bdbc9b086: load sounds right before playing them +- 6154979119: Fix users being created without the `roles` field +- 6bcdd88531: Fixed CAS login after popup closes +- 839789c988: Fix moment timestamps language change +- f0025d4d92: Fixed message fetching method in LivechatBridge for Apps +- 9c957b9d9a: Fix pruning messages in a room results in an incorrect message counter +- 583a3149fe: fix: rejected conference calls continue to ring +- b59fd5d7fb: User information crashing for some locales +- 4349443629: Fix performance issue on Engagement Dashboard aggregation +- 69447e1864: Added ability to disable private app installation via envvar (DISABLE_PRIVATE_APP_INSTALLATION) +- 52a1aa94eb: improve: System messages for omni-visitor abandonment feature +- 7dffec2e2f: chore: Add danger variant to apps action button menus +- f0c8867bb9: Disabled call to tags enterprise endpoint when on community license +- 5e89694bfa: Fixes SAML full name updates not being mirrored to DM rooms. +- d6f0c6afe2: Fixed Importer Progress Bar progress indicator +- 177506ea91: Make user default role setting public +- 3fb2124166: Fixed misleading of 'total' in team members list inside Channel +- 5cee21468e: Fix spotlight search does not find rooms with special or non-latin characters +- cf59c8abe3: Fix engagement dashboard not showing data +- dfb9a075b3: fixed wrong user status displayed during mentioning a user in a channel +- 1fbbb6241a: Don't allow to report self messages +- 53e0c346e2: fixed scrollbar over content in Federated Room List +- 5321e87363: Fix seat counter including bots users +- 7137a193a7: feat: Add flag to disable teams mention via troubleshoot page +- 59e6fe3d2a: fixed layout changing from embedded view when navigating +- 3245a0a318: Fix LinkedIn OAuth broken +- 45a8943ed4: Removed old/deprecated Rocket.Chat Federation card from Info page +- 6eea189ec8: Fix the code that was setting email URL to an invalid value when SMTP was not set +- f5a886a144: fixed an issue where oauth login was not working with some providers +- ba24f3c21f: Fixed `default` field not being returned from the `setDefault` endpoints when setting to false +- a79f61461d: Fixed an issue where timeout for http requests in Apps-Engine bridges was too short +- 51b988b3df: Fix importer filters not working +- 5d857f462c: fix: stop blinking "Room not found" before dm creation +- db26f8a8ee: fixed an issue with the positioning of the message menu +- aaefe865a7: fix: agent role being removed upon user deactivation +- 306a5830c3: Fix `mention-here` and `mention-all` permissions not being honored +- 761cad4382: Fix CORS headers not being set for assets +- 9e5718002a: Fixed Slackbridge was not handling correctly received events from Slack anymore. Events: (Send, edit, delete, react meassages) +- 54ef89c9a7: fix: show requested filters only on requested apps view +- 1589279b79: Fix users not able to login after block time perdiod has passed +- 880ab5689c: Fixed selected departments not being displayed due to pagination +- a81bad24e0: Fixed Apps-Engine event `IPostUserCreated` execution +- 7a4fdf41f8: Fix validation in app status call that allowed Enterprise apps to be enabled in invalid environments +- e28f8d95f0: Fixed inviter not informed when inviting member to room via `/invite` slashcommand +- d47d2021ac: Fixed "teams" icon not being displayed on spotlight sidebar search +- 93d5a5ceb8: fix: User timezone not being respected on Current Chat's filter +- f556518fa1: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + +- b747f3d3bc: Fixed unable to create admin user using ADMIN\_\* environment variables +- 2cf2643399: Fixed failing user data exports +- ace35997a6: chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. +- f5a886a144: fixed an issue on oauth login that caused missing emails to be detected as changed data +- 61128364d6: Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. +- 9496f1eb97: Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data +- 01dec055a0: Fixed Accounts profile form name change was not working +- e4837a15ed: Fixed user mentioning when prepending the username with `>` +- d45365436e: Use group filter when set to LDAP sync process +- c536a4a237: fix: Missing padding on Omnichannel contacts Contextualbar loading state +- 87e4a4aa56: Fixes a problem that allowed users to send empty spaces as comment to bypass the "comment required" setting +- 69a5213afc: Fixed an issue where a mailer error was being sent to customers using offline message's form on Omnichannel instead of the translated one +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- 22cf158c43: fixed the unread messages mark not showing +- 72a34a02f7: fixed the video recorder window not closing after permission is denied. +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [1246a21648] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [f9a748526d] +- Updated dependencies [5cee21468e] +- Updated dependencies [dc1d8ce92e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [074db3b419] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [dce4a829fa] +- Updated dependencies [d45365436e] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [93d4912e17] +- Updated dependencies [ee3815fce4] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/fuselage-ui-kit@2.0.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/ui-client@2.0.0-rc.0 + - @rocket.chat/ui-contexts@2.0.0-rc.0 + - @rocket.chat/ui-theming@0.1.0-rc.0 + - @rocket.chat/i18n@0.0.2-rc.0 + - @rocket.chat/web-ui-registration@2.0.0-rc.0 + - @rocket.chat/api-client@0.1.5-rc.0 + - @rocket.chat/omnichannel-services@0.0.11-rc.0 + - @rocket.chat/pdf-worker@0.0.11-rc.0 + - @rocket.chat/presence@0.0.11-rc.0 + - @rocket.chat/cron@0.0.7-rc.0 + - @rocket.chat/gazzodown@2.0.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + - @rocket.chat/ui-video-conf@2.0.0-rc.0 + - @rocket.chat/base64@1.0.12 + - @rocket.chat/instance-status@0.0.11-rc.0 + - @rocket.chat/random@1.2.1 + - @rocket.chat/sha256@1.0.9 + - @rocket.chat/ui-composer@0.0.1 + +## 6.3.8 + +### Patch Changes + +- ff8e9d9f54: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. + - @rocket.chat/core-typings@6.3.8 + - @rocket.chat/rest-typings@6.3.8 + - @rocket.chat/api-client@0.1.8 + - @rocket.chat/omnichannel-services@0.0.14 + - @rocket.chat/pdf-worker@0.0.14 + - @rocket.chat/presence@0.0.14 + - @rocket.chat/core-services@0.1.8 + - @rocket.chat/cron@0.0.10 + - @rocket.chat/gazzodown@1.0.8 + - @rocket.chat/model-typings@0.0.14 + - @rocket.chat/ui-contexts@1.0.8 + - @rocket.chat/fuselage-ui-kit@1.0.8 + - @rocket.chat/models@0.0.14 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.8 + - @rocket.chat/ui-video-conf@1.0.8 + - @rocket.chat/web-ui-registration@1.0.8 + - @rocket.chat/instance-status@0.0.14 + +## 6.3.7 + +### Patch Changes + +- f1e36a5e46: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- e1acdda0a3: User information crashing for some locales +- deffcb187c: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + +- Updated dependencies [c655be17ca] +- Updated dependencies [deffcb187c] + - @rocket.chat/presence@0.0.13 + - @rocket.chat/cron@0.0.9 + - @rocket.chat/core-typings@6.3.7 + - @rocket.chat/rest-typings@6.3.7 + - @rocket.chat/api-client@0.1.7 + - @rocket.chat/omnichannel-services@0.0.13 + - @rocket.chat/pdf-worker@0.0.13 + - @rocket.chat/core-services@0.1.7 + - @rocket.chat/gazzodown@1.0.7 + - @rocket.chat/model-typings@0.0.13 + - @rocket.chat/ui-contexts@1.0.7 + - @rocket.chat/fuselage-ui-kit@1.0.7 + - @rocket.chat/models@0.0.13 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.7 + - @rocket.chat/ui-video-conf@1.0.7 + - @rocket.chat/web-ui-registration@1.0.7 + - @rocket.chat/instance-status@0.0.13 + +## 6.3.6 + +### Patch Changes + +- 3bbe12e850: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- 285e591a73: Fix engagement dashboard not showing data + - @rocket.chat/core-typings@6.3.6 + - @rocket.chat/rest-typings@6.3.6 + - @rocket.chat/api-client@0.1.6 + - @rocket.chat/omnichannel-services@0.0.12 + - @rocket.chat/pdf-worker@0.0.12 + - @rocket.chat/presence@0.0.12 + - @rocket.chat/core-services@0.1.6 + - @rocket.chat/cron@0.0.8 + - @rocket.chat/gazzodown@1.0.6 + - @rocket.chat/model-typings@0.0.12 + - @rocket.chat/ui-contexts@1.0.6 + - @rocket.chat/fuselage-ui-kit@1.0.6 + - @rocket.chat/models@0.0.12 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.6 + - @rocket.chat/ui-video-conf@1.0.6 + - @rocket.chat/web-ui-registration@1.0.6 + - @rocket.chat/instance-status@0.0.12 + +## 6.3.5 + +### Patch Changes + +- 4cb0b6ba6f: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- f75564c449: Fix a bug that prevented the error message from being shown in the private app installation page +- 03923405e8: Fixed selected departments not being displayed due to pagination +- 92d25b9c7a: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/omnichannel-services@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/presence@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/cron@0.0.7 + - @rocket.chat/instance-status@0.0.11 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + - @rocket.chat/api-client@0.1.5 + - @rocket.chat/pdf-worker@0.0.11 + - @rocket.chat/gazzodown@1.0.5 + - @rocket.chat/ui-contexts@1.0.5 + - @rocket.chat/fuselage-ui-kit@1.0.5 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.5 + - @rocket.chat/ui-video-conf@1.0.5 + - @rocket.chat/web-ui-registration@1.0.5 + ## 6.3.4 ### Patch Changes diff --git a/apps/meteor/app/api/server/default/info.ts b/apps/meteor/app/api/server/default/info.ts index b7806ab08f32..8297f90fffd9 100644 --- a/apps/meteor/app/api/server/default/info.ts +++ b/apps/meteor/app/api/server/default/info.ts @@ -8,7 +8,6 @@ API.default.addRoute( { async get() { const user = await getLoggedInUser(this.request); - return API.v1.success(await getServerInfo(user?._id)); }, }, diff --git a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts new file mode 100644 index 000000000000..ca55cfa33e3e --- /dev/null +++ b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const hasAllPermissionAsyncMock = sinon.stub(); +const getCachedSupportedVersionsTokenMock = sinon.stub(); + +const { getServerInfo } = proxyquire.noCallThru().load('./getServerInfo', { + '../../../utils/rocketchat.info': { + Info: { + version: '3.0.1', + }, + }, + '../../../authorization/server/functions/hasPermission': { + hasPermissionAsync: hasAllPermissionAsyncMock, + }, + '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken': { + getCachedSupportedVersionsToken: getCachedSupportedVersionsTokenMock, + }, + '../../../settings/server': { + settings: new Map(), + }, +}); +describe('#getServerInfo()', () => { + beforeEach(() => { + hasAllPermissionAsyncMock.reset(); + getCachedSupportedVersionsTokenMock.reset(); + }); + + it('should return only the version (without the patch info) when the user is not present', async () => { + expect(await getServerInfo(undefined)).to.be.eql({ version: '3.0' }); + }); + + it('should return only the version (without the patch info) when the user present but they dont have permission', async () => { + hasAllPermissionAsyncMock.resolves(false); + expect(await getServerInfo('userId')).to.be.eql({ version: '3.0' }); + }); + + it('should return the info object + the supportedVersions from the cloud when the request to the cloud was a success', async () => { + const signedJwt = 'signedJwt'; + hasAllPermissionAsyncMock.resolves(true); + getCachedSupportedVersionsTokenMock.resolves(signedJwt); + expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1', supportedVersions: signedJwt } }); + }); + + it('should return the info object ONLY from the cloud when the request to the cloud was NOT a success', async () => { + hasAllPermissionAsyncMock.resolves(true); + getCachedSupportedVersionsTokenMock.rejects(); + expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1' } }); + }); +}); diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts index 39f4b82b350b..53ba3656babe 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.ts @@ -1,23 +1,37 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Info } from '../../../utils/rocketchat.info'; +import { + getCachedSupportedVersionsToken, + wrapPromise, +} from '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken'; +import { Info, minimumClientVersions } from '../../../utils/rocketchat.info'; -type ServerInfo = - | { - info: typeof Info; - } - | { - version: string | undefined; - }; +type ServerInfo = { + info?: typeof Info; + supportedVersions?: { signed: string }; + minimumClientVersions: typeof minimumClientVersions; + version: string; +}; const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1'); export async function getServerInfo(userId?: string): Promise { - if (userId && (await hasPermissionAsync(userId, 'get-server-info'))) { - return { - info: Info, - }; - } + const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics')); + const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken()); + return { version: removePatchInfo(Info.version), + + ...(hasPermissionToViewStatistics && { + info: { + ...Info, + }, + version: Info.version, + }), + + minimumClientVersions, + ...(supportedVersionsToken.success && + supportedVersionsToken.result && { + supportedVersions: { signed: supportedVersionsToken.result }, + }), }; } diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 70b7fc875082..8e0541b8040b 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,4 +1,4 @@ -import { Team } from '@rocket.chat/core-services'; +import { Team, Room } from '@rocket.chat/core-services'; import type { IRoom, ISubscription, IUser, RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { @@ -31,7 +31,6 @@ import { saveRoomSettings } from '../../../channel-settings/server/methods/saveR import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { addUsersToRoomMethod } from '../../../lib/server/methods/addUsersToRoom'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; -import { joinRoomMethod } from '../../../lib/server/methods/joinRoom'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -209,7 +208,7 @@ API.v1.addRoute( const { joinCode, ...params } = this.bodyParams; const findResult = await findChannelByIdOrName({ params }); - await joinRoomMethod(this.userId, findResult._id, joinCode); + await Room.join({ room: findResult, user: this.user, joinCode }); return API.v1.success({ channel: await findChannelByIdOrName({ params, userId: this.userId }), @@ -671,7 +670,14 @@ async function createChannelValidator(params: { async function createChannel( userId: string, - params: { name?: string; members?: string[]; customFields?: Record; extraData?: Record; readOnly?: boolean }, + params: { + name?: string; + members?: string[]; + customFields?: Record; + extraData?: Record; + readOnly?: boolean; + excludeSelf?: boolean; + }, ): Promise<{ channel: IRoom }> { const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false; const id = await createChannelMethod( @@ -681,6 +687,7 @@ async function createChannel( readOnly, params.customFields, params.extraData, + params.excludeSelf, ); return { diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index 02fc30763eeb..7be5b1fc13fe 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -1,7 +1,7 @@ import { Federation, FederationEE } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings'; -import { isEnterprise } from '../../../../ee/app/license/server'; import { API } from '../api'; API.v1.addRoute( @@ -14,7 +14,7 @@ API.v1.addRoute( async get() { const { matrixIds } = this.queryParams; - const federationService = isEnterprise() ? FederationEE : Federation; + const federationService = License.hasValidLicense() ? FederationEE : Federation; const results = await federationService.verifyMatrixIds(matrixIds); diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index df54b683fda4..8f2999cee71e 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,4 +1,4 @@ -import { Team } from '@rocket.chat/core-services'; +import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { check, Match } from 'meteor/check'; @@ -26,29 +26,7 @@ import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; -// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -async function findPrivateGroupByIdOrName({ - params, - checkedArchived = true, - userId, -}: { - params: - | { - roomId?: string; - } - | { - roomName?: string; - }; - userId: string; - checkedArchived?: boolean; -}): Promise<{ - rid: string; - open: boolean; - ro: boolean; - t: string; - name: string; - broadcast: boolean; -}> { +async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( (!('roomId' in params) && !('roomName' in params)) || ('roomId' in params && !(params as { roomId?: string }).roomId && 'roomName' in params && !(params as { roomName?: string }).roomName) @@ -68,17 +46,48 @@ async function findPrivateGroupByIdOrName({ broadcast: 1, }, }; - let room: IRoom | null = null; - if ('roomId' in params) { - room = await Rooms.findOneById(params.roomId || '', roomOptions); - } else if ('roomName' in params) { - room = await Rooms.findOneByName(params.roomName || '', roomOptions); - } + + const room = await (() => { + if ('roomId' in params) { + return Rooms.findOneById(params.roomId || '', roomOptions); + } + if ('roomName' in params) { + return Rooms.findOneByName(params.roomName || '', roomOptions); + } + })(); if (!room || room.t !== 'p') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } + return room; +} + +// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property +async function findPrivateGroupByIdOrName({ + params, + checkedArchived = true, + userId, +}: { + params: + | { + roomId?: string; + } + | { + roomName?: string; + }; + userId: string; + checkedArchived?: boolean; +}): Promise<{ + rid: string; + open: boolean; + ro: boolean; + t: string; + name: string; + broadcast: boolean; +}> { + const room = await getRoomFromParams(params); + const user = await Users.findOneById(userId, { projections: { username: 1 } }); if (!room || !user || !(await canAccessRoomAsync(room, user))) { @@ -302,10 +311,6 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!(await hasPermissionAsync(this.userId, 'create-p'))) { - return API.v1.unauthorized(); - } - if (!this.bodyParams.name) { return API.v1.failure('Body param "name" is required'); } @@ -323,24 +328,32 @@ API.v1.addRoute( const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false; - const result = await createPrivateGroupMethod( - this.userId, - this.bodyParams.name, - this.bodyParams.members ? this.bodyParams.members : [], - readOnly, - this.bodyParams.customFields, - this.bodyParams.extraData, - ); - - const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + try { + const result = await createPrivateGroupMethod( + this.user, + this.bodyParams.name, + this.bodyParams.members ? this.bodyParams.members : [], + readOnly, + this.bodyParams.customFields, + this.bodyParams.extraData, + this.bodyParams.excludeSelf ?? false, + ); + + const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + if (!room) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } - if (!room) { - throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + return API.v1.success({ + group: await composeRoomWithLastMessage(room, this.userId), + }); + } catch (error: unknown) { + if (isMeteorError(error) && error.reason === 'error-not-allowed') { + return API.v1.unauthorized(); + } } - return API.v1.success({ - group: await composeRoomWithLastMessage(room, this.userId), - }); + return API.v1.internalError(); }, }, ); @@ -581,17 +594,14 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const findResult = await findPrivateGroupByIdOrName({ - params: this.bodyParams, - userId: this.userId, - }); + const room = await getRoomFromParams(this.bodyParams); const user = await getUserFromParams(this.bodyParams); if (!user?.username) { return API.v1.failure('Invalid user'); } - await removeUserFromRoomMethod(this.userId, { rid: findResult.rid, username: user.username }); + await removeUserFromRoomMethod(this.userId, { rid: room._id, username: user.username }); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index dec4da6bf87b..ae5a79719cce 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -1,13 +1,14 @@ import crypto from 'crypto'; import type { IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import { Settings, Users } from '@rocket.chat/models'; import { isShieldSvgProps, isSpotlightProps, isDirectoryProps, isMethodCallProps, isMethodCallAnonProps, + isFingerprintProps, isMeteorCall, validateParamsPwGetPolicyRest, } from '@rocket.chat/rest-typings'; @@ -16,6 +17,7 @@ import EJSON from 'ejson'; import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; +import { v4 as uuidv4 } from 'uuid'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; @@ -643,3 +645,75 @@ API.v1.addRoute( }, }, ); + +/** + * @openapi + * /api/v1/fingerprint: + * post: + * description: Update Fingerprint definition as a new workspace or update of configuration + * security: + * $ref: '#/security/authenticated' + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * setDeploymentAs: + * type: string + * example: | + * { + * "setDeploymentAs": "new-workspace" + * } + * responses: + * 200: + * description: Workspace successfully configured + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'fingerprint', + { + authRequired: true, + validateParams: isFingerprintProps, + }, + { + async post() { + check(this.bodyParams, { + setDeploymentAs: String, + }); + + if (this.bodyParams.setDeploymentAs === 'new-workspace') { + await Promise.all([ + Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()), + // Settings.resetValueById('Cloud_Url'), + Settings.resetValueById('Cloud_Service_Agree_PrivacyTerms'), + Settings.resetValueById('Cloud_Workspace_Id'), + Settings.resetValueById('Cloud_Workspace_Name'), + Settings.resetValueById('Cloud_Workspace_Client_Id'), + Settings.resetValueById('Cloud_Workspace_Client_Secret'), + Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At'), + Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri'), + Settings.resetValueById('Cloud_Workspace_PublicKey'), + Settings.resetValueById('Cloud_Workspace_License'), + Settings.resetValueById('Cloud_Workspace_Had_Trial'), + Settings.resetValueById('Cloud_Workspace_Access_Token'), + Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)), + Settings.resetValueById('Cloud_Workspace_Registration_State'), + ]); + } + + await Settings.updateValueById('Deployment_FingerPrint_Verified', true); + + return API.v1.success({}); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index 6ffd0005c764..e1ee82d72478 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -78,7 +78,6 @@ API.v1.addRoute( } try { - logger.debug(`Setting extension ${extension} for agent with id ${user._id}`); await Users.setExtension(user._id, extension); return API.v1.success(); } catch (e) { @@ -146,7 +145,6 @@ API.v1.addRoute( return API.v1.notFound(); } if (!user.extension) { - logger.debug(`User ${user._id} is not associated with any extension. Skipping`); return API.v1.success(); } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 71f7387e1aa5..76a0545c8801 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -223,7 +223,7 @@ export class AppLivechatBridge extends LivechatBridge { } return Promise.all( - (await LivechatVisitors.find(query).toArray()).map( + (await LivechatVisitors.findEnabled(query).toArray()).map( async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor), ), ); diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 7703b06e89a5..e4d09018176d 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -1,4 +1,4 @@ -import type { IMessage, IDirectMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; @@ -17,7 +17,7 @@ export class AppMessageBridge extends MessageBridge { super(); } - protected async create(message: IMessage | IDirectMessage, appId: string): Promise { + protected async create(message: IMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is creating a new message.`); const convertedMessage = await this.orch.getConverters()?.get('messages').convertAppMessage(message); diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 481292d61790..91b0049513f0 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -55,7 +55,11 @@ export class AppRoomBridge extends RoomBridge { } private async createPrivateGroup(userId: string, room: ICoreRoom, members: string[]): Promise { - return (await createPrivateGroupMethod(userId, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('Invalid user'); + } + return (await createPrivateGroupMethod(user, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; } protected async getById(roomId: string, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index ae38feff5eff..905534212836 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -37,7 +37,7 @@ export class AppRoomsConverter { let v; if (room.visitor) { - const visitor = await LivechatVisitors.findOneById(room.visitor.id); + const visitor = await LivechatVisitors.findOneEnabledById(room.visitor.id); const { lastMessageTs, phone } = room.visitorChannelInfo; diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index ba288c96d7b8..a9f5d450efad 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -9,7 +9,7 @@ export class AppVisitorsConverter { } async convertById(id) { - const visitor = await LivechatVisitors.findOneById(id); + const visitor = await LivechatVisitors.findOneEnabledById(id); return this.convertVisitor(visitor); } diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index fc917028c33f..7b5f1594e5c3 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -10,6 +10,8 @@ export const permissions = [ { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'add-user-to-any-c-room', roles: ['admin'] }, { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'kick-user-from-any-c-room', roles: ['admin'] }, + { _id: 'kick-user-from-any-p-room', roles: [] }, { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, { _id: 'archive-room', roles: ['admin', 'owner'] }, { _id: 'assign-admin-role', roles: ['admin'] }, diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index 3e9c9dbd8a35..f885a23b8e6b 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.ts +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -87,7 +87,7 @@ class MsAutoTranslate extends AutoTranslate { if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } - const request = await fetch(this.apiEndPointUrl); + const request = await fetch(this.apiGetLanguages); if (!request.ok) { throw new Error(request.statusText); } diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index d65897b72094..c2bd91e82dd8 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -1,50 +1,59 @@ -import type { SettingValue } from '@rocket.chat/core-typings'; import { Statistics, Users } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { statistics } from '../../../statistics/server'; +import { Info } from '../../../utils/rocketchat.info'; import { LICENSE_VERSION } from '../license'; -type WorkspaceRegistrationData = { +export type WorkspaceRegistrationData = { uniqueId: string; - workspaceId: SettingValue; - address: SettingValue; + deploymentFingerprintHash: string; + deploymentFingerprintVerified: boolean; + workspaceId: string; + address: string; contactName: string; contactEmail: T; seats: number; - allowMarketing: SettingValue; - accountName: SettingValue; - organizationType: unknown; - industry: unknown; - orgSize: unknown; - country: unknown; - language: unknown; - agreePrivacyTerms: SettingValue; - website: SettingValue; - siteName: SettingValue; + + organizationType: string; + industry: string; + orgSize: string; + country: string; + language: string; + allowMarketing: string; + accountName: string; + agreePrivacyTerms: string; + website: string; + siteName: string; workspaceType: unknown; deploymentMethod: string; deploymentPlatform: string; - version: unknown; + version: string; licenseVersion: number; enterpriseReady: boolean; setupComplete: boolean; connectionDisable: boolean; - npsEnabled: SettingValue; + npsEnabled: string; + // TODO: Evaluate naming + MAC: number; + // activeContactsBillingMonth: number; + // activeContactsYesterday: number; }; export async function buildWorkspaceRegistrationData(contactEmail: T): Promise> { const stats = (await Statistics.findLast()) || (await statistics.get()); - const address = settings.get('Site_Url'); - const siteName = settings.get('Site_Name'); - const workspaceId = settings.get('Cloud_Workspace_Id'); - const allowMarketing = settings.get('Allow_Marketing_Emails'); - const accountName = settings.get('Organization_Name'); - const website = settings.get('Website'); - const npsEnabled = settings.get('NPS_survey_enabled'); - const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms'); - const setupWizardState = settings.get('Show_Setup_Wizard'); + const address = settings.get('Site_Url'); + const siteName = settings.get('Site_Name'); + const workspaceId = settings.get('Cloud_Workspace_Id'); + const allowMarketing = settings.get('Allow_Marketing_Emails'); + const accountName = settings.get('Organization_Name'); + const website = settings.get('Website'); + const npsEnabled = settings.get('NPS_survey_enabled'); + const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms'); + const setupWizardState = settings.get('Show_Setup_Wizard'); + const deploymentFingerprintHash = settings.get('Deployment_FingerPrint_Hash'); + const deploymentFingerprintVerified = settings.get('Deployment_FingerPrint_Verified'); const firstUser = await Users.getOldest({ projection: { name: 1, emails: 1 } }); const contactName = firstUser?.name || ''; @@ -54,6 +63,8 @@ export async function buildWorkspaceRegistrationData { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/clients`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } } - // shouldn't get here due to checking this on the method - // but this is just to double check - if (!token) { - return new Error('Invalid token; the registration token is required.'); + const payload = await response.json(); + + if (!payload) { + return undefined; } - const redirectUri = getRedirectUri(); + return payload; +}; - const regInfo = { - email: settings.get('Organization_Email'), - client_name: settings.get('Site_Name'), - redirect_uris: [redirectUri], - }; +export async function connectWorkspace(token: string) { + if (!token) { + throw new CloudWorkspaceConnectionError('Invalid registration token'); + } - const cloudUrl = settings.get('Cloud_Url'); - let result; try { - const request = await fetch(`${cloudUrl}/api/oauth/clients`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - body: regInfo, - }); + const redirectUri = getRedirectUri(); + + const body = { + email: settings.get('Organization_Email'), + client_name: settings.get('Site_Name'), + redirect_uris: [redirectUri], + }; + + const payload = await fetchRegistrationDataPayload({ token, body }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!payload) { + return false; } - result = await request.json(); - } catch (err: any) { + await saveRegistrationData(payload); + + return true; + } catch (err) { SystemLogger.error({ msg: 'Failed to Connect with Rocket.Chat Cloud', url: '/api/oauth/clients', @@ -52,12 +76,4 @@ export async function connectWorkspace(token: string) { return false; } - - if (!result) { - return false; - } - - await saveRegistrationData(result); - - return true; } diff --git a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts b/apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts deleted file mode 100644 index c72a96297f37..000000000000 --- a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Settings } from '@rocket.chat/models'; - -import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { syncWorkspace } from './syncWorkspace'; - -export async function disconnectWorkspace() { - const { connectToCloud } = await retrieveRegistrationStatus(); - if (!connectToCloud) { - return true; - } - - await Settings.updateValueById('Register_Server', false); - - await syncWorkspace(true); - - return true; -} diff --git a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts index 780aa5c67a99..61b3a77966e7 100644 --- a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts +++ b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts @@ -14,15 +14,15 @@ export async function finishOAuthAuthorization(code: string, state: string) { }); } - const cloudUrl = settings.get('Cloud_Url'); const clientId = settings.get('Cloud_Workspace_Client_Id'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const scope = userScopes.join(' '); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, params: new URLSearchParams({ @@ -35,11 +35,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { }), }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err) { SystemLogger.error({ msg: 'Failed to finish OAuth authorization with Rocket.Chat Cloud', @@ -51,7 +51,7 @@ export async function finishOAuthAuthorization(code: string, state: string) { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + result.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); const uid = Meteor.userId(); if (!uid) { @@ -65,11 +65,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { { $set: { 'services.cloud': { - accessToken: result.access_token, + accessToken: payload.access_token, expiresAt, - scope: result.scope, - tokenType: result.token_type, - refreshToken: result.refresh_token, + scope: payload.scope, + tokenType: payload.token_type, + refreshToken: payload.refresh_token, }, }, }, diff --git a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts index 4a35c9834ba5..2c5d9dec77dc 100644 --- a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts +++ b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts @@ -5,16 +5,16 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; export async function getConfirmationPoll(deviceCode: string): Promise { - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); - if (!request.ok) { - throw new Error((await request.json()).error); + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); + + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get confirmation poll from Rocket.Chat Cloud', @@ -25,9 +25,9 @@ export async function getConfirmationPoll(deviceCode: string): Promise>(userId, { projection: { 'services.cloud': 1 } }); - if (!user?.services?.cloud?.accessToken || !user?.services?.cloud?.refreshToken) { - return ''; - } - - const { accessToken, refreshToken, expiresAt } = user.services.cloud; - - const clientId = settings.get('Cloud_Workspace_Client_Id'); - if (!clientId) { - return ''; - } - - const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); - if (!clientSecret) { - return ''; - } - - const now = new Date(); - - if (now < expiresAt && !forceNew) { - return accessToken; - } - - const cloudUrl = settings.get('Cloud_Url'); - const redirectUri = getRedirectUri(); - - if (scope === '') { - scope = userScopes.join(' '); - } - - let authTokenResult; - try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - method: 'POST', - params: new URLSearchParams({ - client_id: clientId, - client_secret: clientSecret, - refresh_token: refreshToken, - scope, - grant_type: 'refresh_token', - redirect_uri: redirectUri, - }), - }); - - if (!request.ok) { - throw new Error((await request.json()).error); - } - - authTokenResult = await request.json(); - } catch (err: any) { - SystemLogger.error({ - msg: 'Failed to get User AccessToken from Rocket.Chat Cloud', - url: '/api/oauth/token', - err, - }); - - if (err) { - if (err.message.includes('oauth_invalid_client_credentials')) { - SystemLogger.error('Server has been unregistered from cloud'); - await removeWorkspaceRegistrationInfo(); - } - - if (err.message.includes('unauthorized')) { - await userLoggedOut(userId); - } - } - - return ''; - } - - if (save) { - const willExpireAt = new Date(); - willExpireAt.setSeconds(willExpireAt.getSeconds() + authTokenResult.expires_in); - - await Users.updateOne( - { _id: user._id }, - { - $set: { - 'services.cloud': { - accessToken: authTokenResult.access_token, - expiresAt: willExpireAt, - }, - }, - }, - ); - } - - return authTokenResult.access_token; -} diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 1a69d108ae4c..b495e3342d4b 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -10,10 +10,10 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; * @param {boolean} save * @returns string */ -export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) { - const { connectToCloud, workspaceRegistered } = await retrieveRegistrationStatus(); +export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true): Promise { + const { workspaceRegistered } = await retrieveRegistrationStatus(); - if (!connectToCloud || !workspaceRegistered) { + if (!workspaceRegistered) { return ''; } @@ -22,10 +22,11 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save if (expires === null) { throw new Error('Cloud_Workspace_Access_Token_Expires_At is not set'); } + const now = new Date(); if (expires.value && now < expires.value && !forceNew) { - return settings.get('Cloud_Workspace_Access_Token'); + return settings.get('Cloud_Workspace_Access_Token'); } const accessToken = await getWorkspaceAccessTokenWithScope(scope); @@ -39,3 +40,46 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save return accessToken.token; } + +export class CloudWorkspaceAccessTokenError extends Error { + constructor() { + super('Could not get workspace access token'); + } +} + +export async function getWorkspaceAccessTokenOrThrow(forceNew = false, scope = '', save = true): Promise { + const token = await getWorkspaceAccessToken(forceNew, scope, save); + + if (!token) { + throw new CloudWorkspaceAccessTokenError(); + } + + return token; +} + +export const generateWorkspaceBearerHttpHeaderOrThrow = async ( + forceNew = false, + scope = '', + save = true, +): Promise<{ Authorization: string }> => { + const token = await getWorkspaceAccessTokenOrThrow(forceNew, scope, save); + return { + Authorization: `Bearer ${token}`, + }; +}; + +export const generateWorkspaceBearerHttpHeader = async ( + forceNew = false, + scope = '', + save = true, +): Promise<{ Authorization: string } | undefined> => { + const token = await getWorkspaceAccessToken(forceNew, scope, save); + + if (!token) { + return undefined; + } + + return { + Authorization: `Bearer ${token}`, + }; +}; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts index 4a0c4b5fe394..88509902cb6d 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts @@ -8,11 +8,11 @@ import { removeWorkspaceRegistrationInfo } from './removeWorkspaceRegistrationIn import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; export async function getWorkspaceAccessTokenWithScope(scope = '') { - const { connectToCloud, workspaceRegistered } = await retrieveRegistrationStatus(); + const { workspaceRegistered } = await retrieveRegistrationStatus(); const tokenResponse = { token: '', expiresAt: new Date() }; - if (!connectToCloud || !workspaceRegistered) { + if (!workspaceRegistered) { return tokenResponse; } @@ -26,12 +26,11 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { scope = workspaceScopes.join(' '); } - const cloudUrl = settings.get('Cloud_Url'); // eslint-disable-next-line @typescript-eslint/naming-convention const client_secret = settings.get('Cloud_Workspace_Client_Secret'); const redirectUri = getRedirectUri(); - let authTokenResult; + let payload; try { const body = new URLSearchParams(); body.append('client_id', client_id); @@ -40,12 +39,13 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { body.append('grant_type', 'client_credentials'); body.append('redirect_uri', redirectUri); - const result = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', body, }); - authTokenResult = await result.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get Workspace AccessToken from Rocket.Chat Cloud', @@ -64,10 +64,10 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); tokenResponse.expiresAt = expiresAt; - tokenResponse.token = authTokenResult.access_token; + tokenResponse.token = payload.access_token; return tokenResponse; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts index f3b6dfc4238a..639f29402fe9 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts @@ -2,9 +2,9 @@ import { settings } from '../../../settings/server'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; export async function getWorkspaceKey() { - const { connectToCloud, workspaceRegistered } = await retrieveRegistrationStatus(); + const { workspaceRegistered } = await retrieveRegistrationStatus(); - if (!connectToCloud || !workspaceRegistered) { + if (!workspaceRegistered) { return false; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 275e646e5343..f9f0cfadc669 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -1,68 +1,94 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; import { callbacks } from '../../../../lib/callbacks'; +import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceLicenseError } from '../../../../lib/errors/CloudWorkspaceLicenseError'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { LICENSE_VERSION } from '../license'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; +const workspaceLicensePayloadSchema = v.object({ + version: v.number().required(), + address: v.string().required(), + license: v.string().required(), + updatedAt: v.string().format('date-time').required(), + modules: v.string().required(), + expireAt: v.string().format('date-time').required(), +}); + +const assertWorkspaceLicensePayload = compile(workspaceLicensePayloadSchema); + +const fetchCloudWorkspaceLicensePayload = async ({ token }: { token: string }): Promise> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/license`, { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + version: LICENSE_VERSION, + }, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceLicensePayload(payload); + + return payload; +}; + export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> { const currentLicense = await Settings.findOne('Cloud_Workspace_License'); + // it should never happen, since even if the license is not found, it will return an empty settings + if (!currentLicense?._updatedAt) { + throw new CloudWorkspaceLicenseError('Failed to retrieve current license'); + } - const cachedLicenseReturn = async () => { - const license = currentLicense?.value as string; + const fromCurrentLicense = async () => { + const license = currentLicense?.value as string | undefined; if (license) { await callbacks.run('workspaceLicenseChanged', license); } - return { updated: false, license }; + return { updated: false, license: license ?? '' }; }; - const token = await getWorkspaceAccessToken(); - if (!token) { - return cachedLicenseReturn(); - } - - let licenseResult; try { - const request = await fetch(`${settings.get('Cloud_Workspace_Registration_Client_Uri')}/license`, { - headers: { - Authorization: `Bearer ${token}`, - }, - params: { - version: LICENSE_VERSION, - }, - }); + const token = await getWorkspaceAccessToken(); + if (!token) { + return fromCurrentLicense(); + } + + const payload = await fetchCloudWorkspaceLicensePayload({ token }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (currentLicense.value && Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) { + return fromCurrentLicense(); } - licenseResult = await request.json(); - } catch (err: any) { + await Settings.updateValueById('Cloud_Workspace_License', payload.license); + + await callbacks.run('workspaceLicenseChanged', payload.license); + + return { updated: true, license: payload.license }; + } catch (err) { SystemLogger.error({ msg: 'Failed to update license from Rocket.Chat Cloud', url: '/license', err, }); - return cachedLicenseReturn(); - } - - const remoteLicense = licenseResult; - - if (!currentLicense || !currentLicense._updatedAt) { - throw new Error('Failed to retrieve current license'); - } - - if (remoteLicense.updatedAt <= currentLicense._updatedAt) { - return cachedLicenseReturn(); + return fromCurrentLicense(); } - - await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); - - await callbacks.run('workspaceLicenseChanged', remoteLicense.license); - - return { updated: true, license: remoteLicense.license }; } diff --git a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts index db425d2e8a30..7ee02a5e5de4 100644 --- a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts @@ -11,7 +11,7 @@ export async function reconnectWorkspace() { await Settings.updateValueById('Register_Server', true); - await syncWorkspace(true); + await syncWorkspace(); return true; } diff --git a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts index 2a04aa54cfe7..ce415d2aa983 100644 --- a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts +++ b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts @@ -15,16 +15,16 @@ export async function registerPreIntentWorkspaceWizard(): Promise { } const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + method: 'POST', body: regInfo, timeout: 10 * 1000, - method: 'POST', }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } return true; diff --git a/apps/meteor/app/cloud/server/functions/retrieveRegistrationStatus.ts b/apps/meteor/app/cloud/server/functions/retrieveRegistrationStatus.ts index 55698f4d27af..0291534ac637 100644 --- a/apps/meteor/app/cloud/server/functions/retrieveRegistrationStatus.ts +++ b/apps/meteor/app/cloud/server/functions/retrieveRegistrationStatus.ts @@ -3,7 +3,6 @@ import { Users } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; export async function retrieveRegistrationStatus(): Promise<{ - connectToCloud: boolean; workspaceRegistered: boolean; workspaceId: string; uniqueId: string; @@ -11,7 +10,6 @@ export async function retrieveRegistrationStatus(): Promise<{ email: string; }> { const info = { - connectToCloud: settings.get('Register_Server'), workspaceRegistered: !!settings.get('Cloud_Workspace_Client_Id') && !!settings.get('Cloud_Workspace_Client_Secret'), workspaceId: settings.get('Cloud_Workspace_Id'), uniqueId: settings.get('uniqueID'), diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index de9fafc99065..5f5df80d0d3d 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -8,9 +8,9 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; export async function startRegisterWorkspace(resend = false) { - const { workspaceRegistered, connectToCloud } = await retrieveRegistrationStatus(); - if ((workspaceRegistered && connectToCloud) || process.env.TEST_MODE) { - await syncWorkspace(true); + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (workspaceRegistered || process.env.TEST_MODE) { + await syncWorkspace(); return true; } @@ -19,22 +19,21 @@ export async function startRegisterWorkspace(resend = false) { const regInfo = await buildWorkspaceRegistrationData(undefined); - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace`, { method: 'POST', body: regInfo, params: { resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register with Rocket.Chat Cloud', @@ -44,11 +43,11 @@ export async function startRegisterWorkspace(resend = false) { return false; } - if (!result) { + if (!payload) { return false; } - await Settings.updateValueById('Cloud_Workspace_Id', result.id); + await Settings.updateValueById('Cloud_Workspace_Id', payload.id); return true; } diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index 3afe84c409ec..382478db61c7 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -7,22 +7,22 @@ import { buildWorkspaceRegistrationData } from './buildRegistrationData'; export async function startRegisterWorkspaceSetupWizard(resend = false, email: string): Promise { const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { body: regInfo, method: 'POST', params: { resent: resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register workspace intent with Rocket.Chat Cloud', @@ -33,9 +33,9 @@ export async function startRegisterWorkspaceSetupWizard(resend = false, email: s throw err; } - if (!result) { + if (!payload) { throw new Error('Failed to fetch registration intent endpoint'); } - return result; + return payload; } diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts new file mode 100644 index 000000000000..183065fd92a6 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts @@ -0,0 +1,23 @@ +import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communication'; + +import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest'; + +describe('supportedVersionsChooseLatest', () => { + test('should return the latest version', async () => { + const versionFromLicense: SignedSupportedVersions = { + signed: 'signed____', + timestamp: '2021-08-31T18:00:00.000Z', + versions: [], + }; + + const versionFromCloud: SignedSupportedVersions = { + signed: 'signed_------', + timestamp: '2021-08-31T19:00:00.000Z', + versions: [], + }; + + const result = await supportedVersionsChooseLatest(versionFromLicense, versionFromCloud); + + expect(result.timestamp).toBe(versionFromCloud.timestamp); + }); +}); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts new file mode 100644 index 000000000000..f0683535de6b --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts @@ -0,0 +1,9 @@ +import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communication'; + +export const supportedVersionsChooseLatest = async (...tokens: (SignedSupportedVersions | undefined)[]) => { + const [token] = (tokens.filter(Boolean) as SignedSupportedVersions[]).sort((a, b) => { + return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); + }); + + return token; +}; diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts new file mode 100644 index 000000000000..577abd4383d0 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -0,0 +1,130 @@ +import type { SettingValue } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; +import { Settings } from '@rocket.chat/models'; +import type { SignedSupportedVersions, SupportedVersions } from '@rocket.chat/server-cloud-communication'; +import type { Response } from '@rocket.chat/server-fetch'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; + +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { generateWorkspaceBearerHttpHeader } from '../getWorkspaceAccessToken'; +import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest'; + +declare module '@rocket.chat/license' { + interface ILicenseV3 { + supportedVersions?: SignedSupportedVersions; + } +} + +/** HELPERS */ + +export const wrapPromise = ( + promise: Promise, +): Promise< + | { + success: true; + result: T; + } + | { + success: false; + error: any; + } +> => + promise + .then((result) => ({ success: true, result } as const)) + .catch((error) => ({ + success: false, + error, + })); + +export const handleResponse = async (promise: Promise) => { + return wrapPromise( + (async () => { + const request = await promise; + if (!request.ok) { + if (request.size > 0) { + throw new Error((await request.json()).error); + } + throw new Error(request.statusText); + } + + return request.json(); + })(), + ); +}; + +const cacheValueInSettings = ( + key: string, + fn: () => Promise, +): (() => Promise) & { + reset: () => Promise; +} => { + const reset = async () => { + const value = await fn(); + + await Settings.updateValueById(key, value); + + return value; + }; + + return Object.assign( + async () => { + const storedValue = settings.get(key); + + if (storedValue) { + return storedValue; + } + + return reset(); + }, + { + reset, + }, + ); +}; + +/** CODE */ + +const getSupportedVersionsFromCloud = async () => { + if (process.env.CLOUD_SUPPORTED_VERSIONS_TOKEN) { + return { + success: true, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result: JSON.parse(process.env.CLOUD_SUPPORTED_VERSIONS!), + }; + } + + const headers = await generateWorkspaceBearerHttpHeader(); + + const response = await handleResponse( + fetch('https://releases.rocket.chat/v2/server/supportedVersions', { + headers, + }), + ); + + if (!response.success) { + SystemLogger.error({ + msg: 'Failed to communicate with Rocket.Chat Cloud', + url: 'https://releases.rocket.chat/v2/server/supportedVersions', + err: response.error, + }); + } + + return response; +}; + +const getSupportedVersionsToken = async () => { + /** + * Gets the supported versions from the license + * Gets the supported versions from the cloud + * Gets the latest version + * return the token + */ + + const [versionsFromLicense, response] = await Promise.all([License.getLicense(), getSupportedVersionsFromCloud()]); + + return (await supportedVersionsChooseLatest(versionsFromLicense?.supportedVersions, (response.success && response.result) || undefined)) + ?.signed; +}; + +export const getCachedSupportedVersionsToken = cacheValueInSettings('Cloud_Workspace_Supported_Versions_Token', getSupportedVersionsToken); diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts deleted file mode 100644 index 9337fd0a0172..000000000000 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NPS, Banner } from '@rocket.chat/core-services'; -import { Settings } from '@rocket.chat/models'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; -import { settings } from '../../../settings/server'; -import { buildWorkspaceRegistrationData } from './buildRegistrationData'; -import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; -import { getWorkspaceLicense } from './getWorkspaceLicense'; -import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; - -export async function syncWorkspace(reconnectCheck = false) { - const { workspaceRegistered, connectToCloud } = await retrieveRegistrationStatus(); - if (!workspaceRegistered || (!connectToCloud && !reconnectCheck)) { - return false; - } - - const info = await buildWorkspaceRegistrationData(undefined); - - const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); - - let result; - try { - const headers: Record = {}; - const token = await getWorkspaceAccessToken(true); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } else { - return false; - } - - const request = await fetch(`${workspaceUrl}/client`, { - headers, - body: info, - method: 'POST', - }); - - if (!request.ok) { - throw new Error((await request.json()).error); - } - - result = await request.json(); - } catch (err: any) { - SystemLogger.error({ - msg: 'Failed to sync with Rocket.Chat Cloud', - url: '/client', - err, - }); - - return false; - } finally { - // aways fetch the license - await getWorkspaceLicense(); - } - - const data = result; - if (!data) { - return true; - } - - if (data.publicKey) { - await Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); - } - - if (data.trial?.trialId) { - await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); - } - - if (data.nps) { - const { id: npsId, expireAt } = data.nps; - - const startAt = new Date(data.nps.startAt); - - await NPS.create({ - npsId, - startAt, - expireAt: new Date(expireAt), - createdBy: { - _id: 'rocket.cat', - username: 'rocket.cat', - }, - }); - - const now = new Date(); - - if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { - await getAndCreateNpsSurvey(npsId); - } - } - - // add banners - if (data.banners) { - for await (const banner of data.banners) { - const { createdAt, expireAt, startAt } = banner; - - await Banner.create({ - ...banner, - createdAt: new Date(createdAt), - expireAt: new Date(expireAt), - startAt: new Date(startAt), - }); - } - } - - return true; -} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts new file mode 100644 index 000000000000..f3885c1e95c2 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts @@ -0,0 +1,116 @@ +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { handleAnnouncementsOnWorkspaceSync, handleNpsOnWorkspaceSync } from './handleCommsSync'; +import { legacySyncWorkspace } from './legacySyncWorkspace'; + +const workspaceCommPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + nps: v.object({ + id: v.string().required(), + startAt: v.string().format('date-time').required(), + expireAt: v.string().format('date-time').required(), + }), + announcements: v.object({ + create: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + selector: v.object({ + roles: v.array(v.string()), + }), + platform: v.array(v.string().enum('web', 'mobile')).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + createdBy: v.string().enum('cloud', 'system').required(), + createdAt: v.string().format('date-time').required(), + dictionary: v.object({}).additional(v.object({}).additional(v.string())), + view: v.any(), + surface: v.string().enum('banner', 'modal').required(), + }), + ), + delete: v.array(v.string()), + }), +}); + +const assertWorkspaceCommPayload = compile(workspaceCommPayloadSchema); + +const fetchCloudAnnouncementsSync = async ({ + token, + data, +}: { + token: string; + data: Cloud.WorkspaceSyncRequestPayload; +}): Promise> => { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v3/comms/workspace`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: data, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceCommPayload(payload); + return payload; +}; + +export async function announcementSync() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const { nps, announcements } = await fetchCloudAnnouncementsSync({ + token, + data: workspaceRegistrationData, + }); + + if (nps) { + await handleNpsOnWorkspaceSync(nps); + } + + if (announcements) { + await handleAnnouncementsOnWorkspaceSync(announcements); + } + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/comms/workspace', + err, + }); + } + + await legacySyncWorkspace(); +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts new file mode 100644 index 000000000000..c8b07f8826cf --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts @@ -0,0 +1,65 @@ +import { NPS, Banner } from '@rocket.chat/core-services'; +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { CloudAnnouncements } from '@rocket.chat/models'; + +import { getAndCreateNpsSurvey } from '../../../../../server/services/nps/getAndCreateNpsSurvey'; + +export const handleNpsOnWorkspaceSync = async (nps: Exclude['nps'], undefined>) => { + const { id: npsId, expireAt } = nps; + + const startAt = new Date(nps.startAt); + + await NPS.create({ + npsId, + startAt, + expireAt: new Date(expireAt), + createdBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + }); + + const now = new Date(); + + if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { + await getAndCreateNpsSurvey(npsId); + } +}; + +export const handleBannerOnWorkspaceSync = async (banners: Exclude['banners'], undefined>) => { + for await (const banner of banners) { + const { createdAt, expireAt, startAt, inactivedAt, _updatedAt, ...rest } = banner; + + await Banner.create({ + ...rest, + createdAt: new Date(createdAt), + expireAt: new Date(expireAt), + startAt: new Date(startAt), + ...(inactivedAt && { inactivedAt: new Date(inactivedAt) }), + }); + } +}; + +const deserializeAnnouncement = (announcement: Serialized): Cloud.Announcement => ({ + ...announcement, + _updatedAt: new Date(announcement._updatedAt), + expireAt: new Date(announcement.expireAt), + startAt: new Date(announcement.startAt), + createdAt: new Date(announcement.createdAt), +}); + +export const handleAnnouncementsOnWorkspaceSync = async ( + announcements: Exclude['announcements'], undefined>, +) => { + const { create, delete: deleteIds } = announcements; + + if (deleteIds) { + await CloudAnnouncements.deleteMany({ _id: { $in: deleteIds } }); + } + + for await (const announcement of create.map(deserializeAnnouncement)) { + const { _id, ...rest } = announcement; + + await CloudAnnouncements.updateOne({ _id }, { $set: rest }, { upsert: true }); + } +}; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts new file mode 100644 index 000000000000..bdd898b510f7 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts @@ -0,0 +1,19 @@ +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { CloudWorkspaceAccessTokenError } from '../getWorkspaceAccessToken'; +import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; +import { announcementSync } from './announcementSync'; +import { syncCloudData } from './syncCloudData'; + +export async function syncWorkspace() { + try { + await syncCloudData(); + await announcementSync(); + } catch (err) { + if (err instanceof CloudWorkspaceAccessTokenError) { + // TODO: Remove License if there is no access token + } + SystemLogger.error({ msg: 'Error during workspace sync', err }); + } + + await getCachedSupportedVersionsToken.reset(); +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts new file mode 100644 index 000000000000..d5f86fad8409 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts @@ -0,0 +1,182 @@ +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import type { WorkspaceRegistrationData } from '../buildRegistrationData'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { getWorkspaceLicense } from '../getWorkspaceLicense'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { handleBannerOnWorkspaceSync, handleNpsOnWorkspaceSync } from './handleCommsSync'; + +const workspaceClientPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + trial: v.object({ + trialing: v.boolean().required(), + trialID: v.string().required(), + endDate: v.string().format('date-time').required(), + marketing: v + .object({ + utmContent: v.string().required(), + utmMedium: v.string().required(), + utmSource: v.string().required(), + utmCampaign: v.string().required(), + }) + .required(), + DowngradesToPlan: v + .object({ + id: v.string().required(), + }) + .required(), + trialRequested: v.boolean().required(), + }), + nps: v.object({ + id: v.string().required(), + startAt: v.string().format('date-time').required(), + expireAt: v.string().format('date-time').required(), + }), + banners: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + platform: v.array(v.string()).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + roles: v.array(v.string()), + createdBy: v.object({ + _id: v.string().required(), + username: v.string(), + }), + createdAt: v.string().format('date-time').required(), + view: v.any(), + active: v.boolean(), + inactivedAt: v.string().format('date-time'), + snapshot: v.string(), + }), + ), + announcements: v.object({ + create: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + selector: v.object({ + roles: v.array(v.string()), + }), + platform: v.array(v.string().enum('web', 'mobile')).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + createdBy: v.string().enum('cloud', 'system').required(), + createdAt: v.string().format('date-time').required(), + dictionary: v.object({}).additional(v.object({}).additional(v.string())), + view: v.any(), + surface: v.string().enum('banner', 'modal').required(), + }), + ), + delete: v.array(v.string()), + }), +}); + +const assertWorkspaceClientPayload = compile(workspaceClientPayloadSchema); + +/** @deprecated */ +const fetchWorkspaceClientPayload = async ({ + token, + workspaceRegistrationData, +}: { + token: string; + workspaceRegistrationData: WorkspaceRegistrationData; +}): Promise | undefined> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/client`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: workspaceRegistrationData, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + if (!payload) { + return undefined; + } + + if (!assertWorkspaceClientPayload(payload)) { + throw new CloudWorkspaceConnectionError('Invalid response from Rocket.Chat Cloud'); + } + + return payload; +}; + +/** @deprecated */ +const consumeWorkspaceSyncPayload = async (result: Serialized) => { + if (result.publicKey) { + await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey); + } + + if (result.trial?.trialID) { + await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + } + + // add banners + if (result.banners) { + await handleBannerOnWorkspaceSync(result.banners); + } + + if (result.nps) { + await handleNpsOnWorkspaceSync(result.nps); + } +}; + +/** @deprecated */ +export async function legacySyncWorkspace() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const payload = await fetchWorkspaceClientPayload({ token, workspaceRegistrationData }); + + if (!payload) { + return true; + } + + await consumeWorkspaceSyncPayload(payload); + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/client', + err, + }); + + return false; + } finally { + await getWorkspaceLicense(); + } +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts new file mode 100644 index 000000000000..5f529a4892ec --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts @@ -0,0 +1,87 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { legacySyncWorkspace } from './legacySyncWorkspace'; + +const workspaceSyncPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + license: v.string().required(), +}); + +const assertWorkspaceSyncPayload = compile(workspaceSyncPayloadSchema); + +const fetchWorkspaceSyncPayload = async ({ + token, + data, +}: { + token: string; + data: Cloud.WorkspaceSyncRequestPayload; +}): Promise> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/sync`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: data, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceSyncPayload(payload); + + return payload; +}; + +export async function syncCloudData() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const { license } = await fetchWorkspaceSyncPayload({ + token, + data: workspaceRegistrationData, + }); + + await callbacks.run('workspaceLicenseChanged', license); + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/sync', + err, + }); + } + + await legacySyncWorkspace(); +} diff --git a/apps/meteor/app/cloud/server/functions/userLogout.ts b/apps/meteor/app/cloud/server/functions/userLogout.ts index e03f96df679d..386137ced604 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.ts +++ b/apps/meteor/app/cloud/server/functions/userLogout.ts @@ -7,9 +7,9 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { userLoggedOut } from './userLoggedOut'; export async function userLogout(userId: string): Promise { - const { connectToCloud, workspaceRegistered } = await retrieveRegistrationStatus(); + const { workspaceRegistered } = await retrieveRegistrationStatus(); - if (!connectToCloud || !workspaceRegistered) { + if (!workspaceRegistered) { return ''; } @@ -26,10 +26,11 @@ export async function userLogout(userId: string): Promise { return ''; } - const cloudUrl = settings.get('Cloud_Url'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const { refreshToken } = user.services.cloud; + + const cloudUrl = settings.get('Cloud_Url'); await fetch(`${cloudUrl}/api/oauth/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/apps/meteor/app/cloud/server/index.ts b/apps/meteor/app/cloud/server/index.ts index 4bb1f634978e..c7e783d4d5aa 100644 --- a/apps/meteor/app/cloud/server/index.ts +++ b/apps/meteor/app/cloud/server/index.ts @@ -2,7 +2,6 @@ import { cronJobs } from '@rocket.chat/cron'; import { Meteor } from 'meteor/meteor'; import { SystemLogger } from '../../../server/lib/logger/system'; -import { settings } from '../../settings/server'; import { connectWorkspace } from './functions/connectWorkspace'; import { getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken'; import { getWorkspaceAccessTokenWithScope } from './functions/getWorkspaceAccessTokenWithScope'; @@ -13,24 +12,6 @@ import './methods'; const licenseCronName = 'Cloud Workspace Sync'; Meteor.startup(async () => { - // run token/license sync if registered - let TroubleshootDisableWorkspaceSync: boolean; - settings.watch('Troubleshoot_Disable_Workspace_Sync', async (value) => { - if (TroubleshootDisableWorkspaceSync === value) { - return; - } - TroubleshootDisableWorkspaceSync = value; - - if (value) { - return cronJobs.remove(licenseCronName); - } - - setImmediate(() => syncWorkspace()); - await cronJobs.add(licenseCronName, '0 */12 * * *', async () => { - await syncWorkspace(); - }); - }); - const { workspaceRegistered } = await retrieveRegistrationStatus(); if (process.env.REG_TOKEN && process.env.REG_TOKEN !== '' && !workspaceRegistered) { @@ -43,9 +24,14 @@ Meteor.startup(async () => { console.log('Successfully registered with token provided by REG_TOKEN!'); } catch (e: any) { - SystemLogger.error('An error occured registering with token.', e.message); + SystemLogger.error('An error occurred registering with token.', e.message); } } + + setImmediate(() => syncWorkspace()); + await cronJobs.add(licenseCronName, '0 */12 * * *', async () => { + await syncWorkspace(); + }); }); export { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope }; diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts index d2fbac1af881..1d328d0c213e 100644 --- a/apps/meteor/app/cloud/server/methods.ts +++ b/apps/meteor/app/cloud/server/methods.ts @@ -6,7 +6,6 @@ import { hasPermissionAsync } from '../../authorization/server/functions/hasPerm import { buildWorkspaceRegistrationData } from './functions/buildRegistrationData'; import { checkUserHasCloudLogin } from './functions/checkUserHasCloudLogin'; import { connectWorkspace } from './functions/connectWorkspace'; -import { disconnectWorkspace } from './functions/disconnectWorkspace'; import { finishOAuthAuthorization } from './functions/finishOAuthAuthorization'; import { getOAuthAuthorizationUrl } from './functions/getOAuthAuthorizationUrl'; import { reconnectWorkspace } from './functions/reconnectWorkspace'; @@ -19,7 +18,6 @@ declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { 'cloud:checkRegisterStatus': () => { - connectToCloud: boolean; workspaceRegistered: boolean; workspaceId: string; uniqueId: string; @@ -110,7 +108,9 @@ Meteor.methods({ }); } - return syncWorkspace(); + await syncWorkspace(); + + return true; }, async 'cloud:connectWorkspace'(token) { check(token, String); @@ -137,22 +137,6 @@ Meteor.methods({ return connectWorkspace(token); }, - async 'cloud:disconnectWorkspace'() { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:connectServer', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:connectServer', - }); - } - - return disconnectWorkspace(); - }, async 'cloud:reconnectWorkspace'() { const uid = Meteor.userId(); if (!uid) { diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 03a42e45a17b..cb6fa94273a2 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -1,4 +1,6 @@ +import { createHash } from 'crypto'; import type http from 'http'; +import type { UrlWithParsedQuery } from 'url'; import url from 'url'; import { Logger } from '@rocket.chat/logger'; @@ -77,6 +79,19 @@ WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerRespo }); const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware; +declare module 'meteor/webapp' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebApp { + function categorizeRequest( + req: http.IncomingMessage, + ): { arch: string; path: string; url: UrlWithParsedQuery } & Record; + } +} + +// These routes already handle cache control on their own +const cacheControlledRoutes: Array = ['/assets', '/custom-sounds', '/emoji-custom', '/avatar', '/file-upload'].map( + (route) => new RegExp(`^${route}`, 'i'), +); // @ts-expect-error - accessing internal property of webapp WebAppInternals.staticFilesMiddleware = function ( @@ -86,6 +101,32 @@ WebAppInternals.staticFilesMiddleware = function ( next: NextFunction, ) { res.setHeader('Access-Control-Allow-Origin', '*'); + const { arch, path, url } = WebApp.categorizeRequest(req); + + if (Meteor.isProduction && !cacheControlledRoutes.some((regexp) => regexp.test(path))) { + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + + // Prevent meteor_runtime_config.js to load from a different expected hash possibly causing + // a cache of the file for the wrong hash and start a client loop due to the mismatch + // of the hashes of ui versions which would be checked against a websocket response + if (path === '/meteor_runtime_config.js') { + const program = WebApp.clientPrograms[arch] as (typeof WebApp.clientPrograms)[string] & { + meteorRuntimeConfigHash?: string; + meteorRuntimeConfig: string; + }; + + if (!program?.meteorRuntimeConfigHash) { + program.meteorRuntimeConfigHash = createHash('sha1') + .update(JSON.stringify(encodeURIComponent(program.meteorRuntimeConfig))) + .digest('hex'); + } + + if (program.meteorRuntimeConfigHash !== url.query.hash) { + res.writeHead(404); + return res.end(); + } + } return _staticFilesMiddleware(staticFiles, req, res, next); }; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts index a4f59136a1f9..f881c15f9886 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts @@ -7,21 +7,22 @@ import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; +const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getURL('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getURL('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getURL('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getURL('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getURL('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getURL('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getURL('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getURL('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getURL('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getURL('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getURL('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getURL('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getURL('sounds/ringtone.mp3') }, + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, ]; class CustomSoundsClass { @@ -85,7 +86,7 @@ class CustomSoundsClass { } getURL(sound: ICustomSound) { - return getURL(`/custom-sounds/${sound._id}.${sound.extension}?_dc=${sound.random || 0}`); + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); } getList() { diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts index 5eb2ef38e5b7..3ad61c4c42f0 100644 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts @@ -59,7 +59,7 @@ Meteor.startup(() => { return false; } - return uid !== user._id ? hasPermission('start-discussion-other-user') : hasPermission('start-discussion'); + return uid !== user._id ? hasPermission('start-discussion-other-user', room._id) : hasPermission('start-discussion', room._id); }, order: 1, group: 'menu', diff --git a/apps/meteor/app/discussion/server/hooks/joinDiscussionOnMessage.ts b/apps/meteor/app/discussion/server/hooks/joinDiscussionOnMessage.ts deleted file mode 100644 index b953d4658c85..000000000000 --- a/apps/meteor/app/discussion/server/hooks/joinDiscussionOnMessage.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Subscriptions } from '@rocket.chat/models'; - -import { callbacks } from '../../../../lib/callbacks'; -import { joinRoomMethod } from '../../../lib/server/methods/joinRoom'; - -callbacks.add( - 'beforeSaveMessage', - async (message, room) => { - // abort if room is not a discussion - if (!room?.prid) { - return message; - } - - // check if user already joined the discussion - const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, message.u._id, { - projection: { _id: 1 }, - }); - - if (sub) { - return message; - } - - await joinRoomMethod(message.u._id, room._id); - - return message; - }, - callbacks.priority.MEDIUM, - 'joinDiscussionOnMessage', -); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index ce5c09947a60..c3869f8ff963 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -156,7 +156,7 @@ const create = async ({ const discussion = await createRoom( type, name, - user.username as string, + user, [...new Set(invitedUsers)].filter(Boolean), false, false, @@ -221,7 +221,7 @@ export const createDiscussion = async ( }); } - if (!(await hasAtLeastOnePermissionAsync(userId, ['start-discussion', 'start-discussion-other-user']))) { + if (!(await hasAtLeastOnePermissionAsync(userId, ['start-discussion', 'start-discussion-other-user'], prid))) { throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' }); } const user = await Users.findOneById(userId); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index f241879cdc67..1b596d625d9b 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1034,7 +1034,11 @@ export class ImportDataConverter { return; } if (roomData.t === 'p') { - roomInfo = await createPrivateGroupMethod(startedByUserId, roomData.name, members, false, {}, {}, true); + const user = await Users.findOneById(startedByUserId); + if (!user) { + throw new Error('importer-channel-invalid-creator'); + } + roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}, true); } else { roomInfo = await createChannelMethod(startedByUserId, roomData.name, members, false, {}, {}, true); } diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index e1db46729011..5162fa54ad9c 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -1,114 +1,21 @@ import { Integrations, Users } from '@rocket.chat/models'; -import * as Models from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import { Livechat } from 'meteor/rocketchat:livechat'; -import moment from 'moment'; import _ from 'underscore'; -import { VM, VMScript } from 'vm2'; -import * as s from '../../../../lib/utils/stringUtils'; -import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { httpCall } from '../../../../server/lib/http/call'; import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { settings } from '../../../settings/server'; +import { IsolatedVMScriptEngine } from '../lib/isolated-vm/isolated-vm'; +import { VM2ScriptEngine } from '../lib/vm2/vm2'; import { incomingLogger } from '../logger'; import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration'; import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration'; -const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); +const vm2Engine = new VM2ScriptEngine(true); +const ivmEngine = new IsolatedVMScriptEngine(true); -export const forbiddenModelMethods = ['registerModel', 'getCollectionName']; - -const compiledScripts = {}; -function buildSandbox(store = {}) { - const httpAsync = async (method, url, options) => { - try { - return { - result: await httpCall(method, url, options), - }; - } catch (error) { - return { error }; - } - }; - - const sandbox = { - scriptTimeout(reject) { - return setTimeout(() => reject('timed out'), 3000); - }, - _, - s, - console, - moment, - Promise, - Livechat, - Store: { - set(key, val) { - store[key] = val; - return val; - }, - get(key) { - return store[key]; - }, - }, - HTTP: (method, url, options) => { - // TODO: deprecate, track and alert - return deasyncPromise(httpAsync(method, url, options)); - }, - // TODO: Export fetch as the non deprecated method - }; - Object.keys(Models) - .filter((k) => !forbiddenModelMethods.includes(k)) - .forEach((k) => { - sandbox[k] = Models[k]; - }); - return { store, sandbox }; -} - -function getIntegrationScript(integration) { - if (DISABLE_INTEGRATION_SCRIPTS) { - throw API.v1.failure('integration-scripts-disabled'); - } - - const compiledScript = compiledScripts[integration._id]; - if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { - return compiledScript.script; - } - - const script = integration.scriptCompiled; - const { sandbox, store } = buildSandbox(); - try { - incomingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); - incomingLogger.debug(script); - - const vmScript = new VMScript(`${script}; Script;`, 'script.js'); - const vm = new VM({ - sandbox, - }); - - const ScriptClass = vm.run(vmScript); - - if (ScriptClass) { - compiledScripts[integration._id] = { - script: new ScriptClass(), - store, - _updatedAt: integration._updatedAt, - }; - - return compiledScripts[integration._id].script; - } - } catch (err) { - incomingLogger.error({ - msg: 'Error evaluating Script in Trigger', - integration: integration.name, - script, - err, - }); - throw API.v1.failure('error-evaluating-script'); - } - - incomingLogger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name }); - throw API.v1.failure('class-script-not-found'); +function getEngine(integration) { + return integration.scriptEngine === 'isolated-vm' ? ivmEngine : vm2Engine; } async function createIntegration(options, user) { @@ -178,20 +85,9 @@ async function executeIntegrationRest() { emoji: this.integration.emoji, }; - if ( - !DISABLE_INTEGRATION_SCRIPTS && - this.integration.scriptEnabled && - this.integration.scriptCompiled && - this.integration.scriptCompiled.trim() !== '' - ) { - let script; - try { - script = getIntegrationScript(this.integration); - } catch (e) { - incomingLogger.error(e); - return API.v1.failure(e.message); - } + const scriptEngine = getEngine(this.integration); + if (scriptEngine.integrationHasValidScript(this.integration)) { this.request.setEncoding('utf8'); const content_raw = this.request.read(); @@ -216,37 +112,12 @@ async function executeIntegrationRest() { }, }; - try { - const { sandbox } = buildSandbox(compiledScripts[this.integration._id].store); - sandbox.script = script; - sandbox.request = request; - - const vm = new VM({ - timeout: 3000, - sandbox, - }); - - const result = await new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - const scriptResult = await vm.run(` - new Promise((resolve, reject) => { - scriptTimeout(reject); - try { - resolve(script.process_incoming_request({ request: request })); - } catch(e) { - reject(e); - } - }).catch((error) => { throw new Error(error); }); - `); - - resolve(scriptResult); - } catch (e) { - reject(e); - } - }); - }); + const result = await scriptEngine.processIncomingRequest({ + integration: this.integration, + request, + }); + try { if (!result) { incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger has no data', diff --git a/apps/meteor/app/integrations/server/lib/ScriptEngine.ts b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts new file mode 100644 index 000000000000..e46984a893ef --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts @@ -0,0 +1,385 @@ +import type { + IUser, + IRoom, + IMessage, + IOutgoingIntegration, + IIncomingIntegration, + IIntegration, + IIntegrationHistory, +} from '@rocket.chat/core-typings'; +import type { Logger } from '@rocket.chat/logger'; +import type { serverFetch } from '@rocket.chat/server-fetch'; +import { wrapExceptions } from '@rocket.chat/tools'; + +import { incomingLogger, outgoingLogger } from '../logger'; +import type { IScriptClass, CompiledScript } from './definition'; +import { updateHistory } from './updateHistory'; + +type OutgoingRequestBaseData = { + token: IOutgoingIntegration['token']; + bot: false; + trigger_word: string; +}; + +type OutgoingRequestSendMessageData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + message_id: string; + timestamp: Date; + user_id: string; + user_name: string; + text: string; + siteUrl: string; + alias?: string; + bot?: boolean; + isEdited?: true; + tmid?: string; +}; + +type OutgoingRequestUploadedFileData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + message_id: string; + timestamp: Date; + user_id: string; + user_name: string; + text: string; + + user: IUser; + room: IRoom; + message: IMessage; + + alias?: string; + bot?: boolean; +}; + +type OutgoingRequestRoomCreatedData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + timestamp: Date; + user_id: string; + user_name: string; + owner: IUser; + room: IRoom; +}; + +type OutgoingRequestRoomData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + timestamp: Date; + user_id: string; + user_name: string; + owner: IUser; + room: IRoom; + bot?: boolean; +}; + +type OutgoingRequestUserCreatedData = OutgoingRequestBaseData & { + timestamp: Date; + user_id: string; + user_name: string; + user: IUser; + bot?: boolean; +}; + +type OutgoingRequestData = + | OutgoingRequestSendMessageData + | OutgoingRequestUploadedFileData + | OutgoingRequestRoomCreatedData + | OutgoingRequestRoomData + | OutgoingRequestUserCreatedData; + +type OutgoingRequest = { + params: Record; + method: 'POST'; + url: string; + data: OutgoingRequestData; + auth: undefined; + headers: Record; +}; + +type OutgoingRequestFromScript = { + url?: string; + headers?: Record; + method?: string; + message?: { + text?: string; + channel?: string; + attachments?: { + color?: string; + author_name?: string; + author_link?: string; + author_icon?: string; + title?: string; + title_link?: string; + text?: string; + fields?: { + title?: string; + value?: string; + short?: boolean; + }[]; + image_url?: string; + thumb_url?: string; + }[]; + }; + + auth?: string; + data?: Record; +}; + +type OutgoingRequestContext = { + integration: IOutgoingIntegration; + data: OutgoingRequestData; + historyId: IIntegrationHistory['_id']; + url: string; +}; + +type ProcessedOutgoingRequest = OutgoingRequest | OutgoingRequestFromScript; + +type OutgoingResponseContext = { + integration: IOutgoingIntegration; + request: ProcessedOutgoingRequest; + response: Awaited>; + content: string; + historyId: IIntegrationHistory['_id']; +}; + +type IncomingIntegrationRequest = { + url: { + hash: string | null | undefined; + search: string | null | undefined; + query: Record; + pathname: string | null | undefined; + path: string | null | undefined; + }; + url_raw: string; + url_params: Record; + content: Record; + content_raw: string; + headers: Record; + body: Record; + user: Pick, '_id' | 'name' | 'username'>; +}; + +export abstract class IntegrationScriptEngine { + protected compiledScripts: Record; + + public get disabled(): boolean { + return this.isDisabled(); + } + + public get incoming(): IsIncoming { + return this.isIncoming; + } + + constructor(private isIncoming: IsIncoming) { + this.compiledScripts = {}; + } + + public integrationHasValidScript(integration: IIntegration): boolean { + return Boolean(!this.disabled && integration.scriptEnabled && integration.scriptCompiled && integration.scriptCompiled.trim() !== ''); + } + + // PrepareOutgoingRequest will execute a script to build the request object that will be used for the actual integration request + // It may also return a message object to be sent to the room where the integration was triggered + public async prepareOutgoingRequest({ integration, data, historyId, url }: OutgoingRequestContext): Promise { + const request: OutgoingRequest = { + params: {}, + method: 'POST', + url, + data, + auth: undefined, + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', + }, + }; + + if (!(await this.hasScriptAndMethod(integration, 'prepare_outgoing_request'))) { + return request; + } + + return this.executeOutgoingScript(integration, 'prepare_outgoing_request', { request }, historyId); + } + + public async processOutgoingResponse({ + integration, + request, + response, + content, + historyId, + }: OutgoingResponseContext): Promise { + if (!(await this.hasScriptAndMethod(integration, 'process_outgoing_response'))) { + return; + } + + const sandbox = { + request, + response: { + error: null, + status_code: response.status, + content, + content_raw: content, + headers: Object.fromEntries(response.headers), + }, + }; + + const scriptResult = await this.executeOutgoingScript(integration, 'process_outgoing_response', sandbox, historyId); + + if (scriptResult === false) { + return scriptResult; + } + + if (scriptResult?.content) { + return scriptResult.content; + } + } + + public async processIncomingRequest({ + integration, + request, + }: { + integration: IIncomingIntegration; + request: IncomingIntegrationRequest; + }): Promise { + return this.executeIncomingScript(integration, 'process_incoming_request', { request }); + } + + protected get logger(): ReturnType { + if (this.isIncoming) { + return incomingLogger; + } + + return outgoingLogger; + } + + protected async executeOutgoingScript( + integration: IOutgoingIntegration, + method: keyof IScriptClass, + params: Record, + historyId: IIntegrationHistory['_id'], + ): Promise { + if (this.disabled) { + return; + } + + const script = await wrapExceptions(() => this.getIntegrationScript(integration)).suppress((e: any) => + updateHistory({ + historyId, + step: 'execute-script-getting-script', + error: true, + errorStack: e, + }), + ); + + if (!script) { + return; + } + + if (!script[method]) { + this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); + await updateHistory({ historyId, step: `execute-script-no-method-${method}` }); + return; + } + + try { + await updateHistory({ historyId, step: `execute-script-before-running-${method}` }); + + const result = await this.runScriptMethod({ + integrationId: integration._id, + script, + method, + params, + }); + + this.logger.debug({ + msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, + result, + }); + + return result; + } catch (err: any) { + await updateHistory({ + historyId, + step: `execute-script-error-running-${method}`, + error: true, + errorStack: err.stack.replace(/^/gm, ' '), + }); + this.logger.error({ + msg: 'Error running Script in the Integration', + integration: integration.name, + err, + }); + this.logger.debug({ + msg: 'Error running Script in the Integration', + integration: integration.name, + script: integration.scriptCompiled, + }); + } + } + + protected async executeIncomingScript( + integration: IIncomingIntegration, + method: keyof IScriptClass, + params: Record, + ): Promise { + if (!this.integrationHasValidScript(integration)) { + return; + } + + const script = await wrapExceptions(() => this.getIntegrationScript(integration)).catch((e) => { + this.logger.error(e); + throw e; + }); + + if (!script[method]) { + this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); + return; + } + + return wrapExceptions(() => + this.runScriptMethod({ + integrationId: integration._id, + script, + method, + params, + }), + ).catch((err: any) => { + this.logger.error({ + msg: 'Error running Script in Trigger', + integration: integration.name, + script: integration.scriptCompiled, + err, + }); + throw new Error('error-running-script'); + }); + } + + protected async hasScriptAndMethod(integration: IIntegration, method: keyof IScriptClass): Promise { + const script = await this.getScriptSafely(integration); + return typeof script?.[method] === 'function'; + } + + protected async getScriptSafely(integration: IIntegration): Promise | undefined> { + if (this.disabled || integration.scriptEnabled !== true || !integration.scriptCompiled || integration.scriptCompiled.trim() === '') { + return; + } + + return wrapExceptions(() => this.getIntegrationScript(integration)).suppress(); + } + + protected abstract isDisabled(): boolean; + + protected abstract runScriptMethod({ + integrationId, + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: IScriptClass; + method: keyof IScriptClass; + params: Record; + }): Promise; + + protected abstract getIntegrationScript(integration: IIntegration): Promise>; +} diff --git a/apps/meteor/app/integrations/server/lib/definition.ts b/apps/meteor/app/integrations/server/lib/definition.ts new file mode 100644 index 000000000000..b4d11b9f4e8b --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/definition.ts @@ -0,0 +1,19 @@ +import type { IIntegration } from '@rocket.chat/core-typings'; + +export interface IScriptClass { + prepare_outgoing_request?: (params: Record) => any; + process_outgoing_response?: (params: Record) => any; + process_incoming_request?: (params: Record) => any; +} + +export type FullScriptClass = Required; + +export type CompiledScript = { + script: Partial; + store: Record; + _updatedAt: IIntegration['_updatedAt']; +}; + +export type CompatibilityScriptResult = IScriptClass & { + availableFunctions: (keyof IScriptClass)[]; +}; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts new file mode 100644 index 000000000000..1bbefb6a2ee7 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts @@ -0,0 +1,127 @@ +import { EventEmitter } from 'events'; + +import { serverFetch as fetch, Response } from '@rocket.chat/server-fetch'; +import ivm, { type Context } from 'isolated-vm'; + +import * as s from '../../../../../lib/utils/stringUtils'; + +const proxyObject = (obj: Record, forbiddenKeys: string[] = []): Record => { + return copyObject({ + isProxy: true, + get: (key: string) => { + if (forbiddenKeys.includes(key)) { + return undefined; + } + + const value = obj[key]; + + if (typeof value === 'function') { + return new ivm.Reference(async (...args: any[]) => { + const result = (obj[key] as any)(...args); + + if (result && result instanceof Promise) { + return new Promise(async (resolve, reject) => { + try { + const awaitedResult = await result; + resolve(makeTransferable(awaitedResult)); + } catch (e) { + reject(e); + } + }); + } + + return makeTransferable(result); + }); + } + + return makeTransferable(value); + }, + }); +}; + +const copyObject = (obj: Record | any[]): Record | any[] => { + if (Array.isArray(obj)) { + return obj.map((data) => copyData(data)); + } + + if (obj instanceof Response) { + return proxyObject(obj, ['clone']); + } + + if (isSemiTransferable(obj)) { + return obj; + } + + if (typeof obj[Symbol.iterator as any] === 'function') { + return copyObject(Array.from(obj as any)); + } + + if (obj instanceof EventEmitter) { + return {}; + } + + const keys = Object.keys(obj); + + return { + ...Object.fromEntries( + keys.map((key) => { + const data = obj[key]; + + if (typeof data === 'function') { + return [key, new ivm.Callback((...args: any[]) => obj[key](...args))]; + } + + return [key, copyData(data)]; + }), + ), + }; +}; + +// Transferable data can be passed to isolates directly +const isTransferable = (data: any): data is ivm.Transferable => { + const dataType = typeof data; + + if (data === ivm) { + return true; + } + + if (['null', 'undefined', 'string', 'number', 'boolean', 'function'].includes(dataType)) { + return true; + } + + if (dataType !== 'object') { + return false; + } + + return ( + data instanceof ivm.Isolate || + data instanceof ivm.Context || + data instanceof ivm.Script || + data instanceof ivm.ExternalCopy || + data instanceof ivm.Callback || + data instanceof ivm.Reference + ); +}; + +// Semi-transferable data can be copied with an ivm.ExternalCopy without needing any manipulation. +const isSemiTransferable = (data: any) => data instanceof ArrayBuffer; + +const copyData = | any[]>(data: T) => (isTransferable(data) ? data : copyObject(data)); +const makeTransferable = (data: any) => (isTransferable(data) ? data : new ivm.ExternalCopy(copyObject(data)).copyInto()); + +export const buildSandbox = (context: Context) => { + const { global: jail } = context; + jail.setSync('global', jail.derefInto()); + jail.setSync('ivm', ivm); + + jail.setSync('s', makeTransferable(s)); + jail.setSync('console', makeTransferable(console)); + + jail.setSync( + 'serverFetch', + new ivm.Reference(async (url: string, ...args: any[]) => { + const result = await fetch(url, ...args); + return makeTransferable(result); + }), + ); +}; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts new file mode 100644 index 000000000000..77ce2475e8c2 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts @@ -0,0 +1,60 @@ +export const getCompatibilityScript = (customScript?: string) => ` + const Store = (function() { + const store = {}; + return { + set(key, val) { + store[key] = val; + return val; + }, + get(key) { + return store[key]; + }, + }; + })(); + + const reproxy = (reference) => { + return new Proxy(reference, { + get(target, p, receiver) { + if (target !== reference || p === 'then') { + return Reflect.get(target, p, receiver); + } + + const data = reference.get(p); + + if (typeof data === 'object' && data instanceof ivm.Reference && data.typeof === 'function') { + return (...args) => data.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } }); + } + + return data; + } + }); + }; + + //url, options, allowSelfSignedCertificate + const fetch = async (...args) => { + const result = await serverFetch.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } }); + + if (result && typeof result === 'object' && result.isProxy) { + return reproxy(result); + } + + return result; + }; + + ${customScript} + + (function() { + const instance = new Script(); + + const functions = { + ...(typeof instance['prepare_outgoing_request'] === 'function' ? { prepare_outgoing_request : (...args) => instance.prepare_outgoing_request(...args) } : {}), + ...(typeof instance['process_outgoing_response'] === 'function' ? { process_outgoing_response : (...args) => instance.process_outgoing_response(...args) } : {}), + ...(typeof instance['process_incoming_request'] === 'function' ? { process_incoming_request : (...args) => instance.process_incoming_request(...args) } : {}), + }; + + return { + ...functions, + availableFunctions: Object.keys(functions), + } + })(); +`; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts new file mode 100644 index 000000000000..2c78b6d98a7c --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts @@ -0,0 +1,99 @@ +import type { IIntegration, ValueOf } from '@rocket.chat/core-typings'; +import { pick } from '@rocket.chat/tools'; +import ivm, { type Reference } from 'isolated-vm'; + +import { IntegrationScriptEngine } from '../ScriptEngine'; +import type { IScriptClass, CompatibilityScriptResult, FullScriptClass } from '../definition'; +import { buildSandbox } from './buildSandbox'; +import { getCompatibilityScript } from './getCompatibilityScript'; + +const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true', 'ivm'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); + +export class IsolatedVMScriptEngine extends IntegrationScriptEngine { + protected isDisabled(): boolean { + return DISABLE_INTEGRATION_SCRIPTS; + } + + protected async callScriptFunction( + scriptReference: Reference>, + ...params: Parameters> + ): Promise { + return scriptReference.applySync(undefined, params, { + arguments: { copy: true }, + result: { copy: true, promise: true }, + }); + } + + protected async runScriptMethod({ + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: Partial; + method: keyof IScriptClass; + params: Record; + }): Promise { + const fn = script[method]; + + if (typeof fn !== 'function') { + throw new Error('integration-method-not-found'); + } + + return fn(params); + } + + protected async getIntegrationScript(integration: IIntegration): Promise> { + if (this.disabled) { + throw new Error('integration-scripts-disabled'); + } + + const compiledScript = this.compiledScripts[integration._id]; + if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { + return compiledScript.script; + } + + const script = integration.scriptCompiled; + try { + this.logger.info({ msg: 'Will evaluate the integration script', integration: pick(integration, 'name', '_id') }); + this.logger.debug(script); + + const isolate = new ivm.Isolate({ memoryLimit: 8 }); + + const ivmScript = await isolate.compileScript(getCompatibilityScript(script)); + + const ivmContext = isolate.createContextSync(); + buildSandbox(ivmContext); + + const ivmResult: Reference = await ivmScript.run(ivmContext, { + reference: true, + timeout: 3000, + }); + + const availableFunctions = await ivmResult.get('availableFunctions', { copy: true }); + const scriptFunctions = Object.fromEntries( + availableFunctions.map((functionName) => { + const fnReference = ivmResult.getSync(functionName, { reference: true }); + return [functionName, (...params: Parameters>) => this.callScriptFunction(fnReference, ...params)]; + }), + ) as Partial; + + this.compiledScripts[integration._id] = { + script: scriptFunctions, + store: {}, + _updatedAt: integration._updatedAt, + }; + + return scriptFunctions; + } catch (err: any) { + this.logger.error({ + msg: 'Error evaluating integration script', + integration: integration.name, + script, + err, + }); + + throw new Error('error-evaluating-script'); + } + } +} diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index b122b22ff355..b5050b8c4716 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -1,30 +1,25 @@ -import { Integrations, IntegrationHistory, Users, Rooms, Messages } from '@rocket.chat/models'; -import * as Models from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; +import { Integrations, Users, Rooms, Messages } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { wrapExceptions } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; import _ from 'underscore'; -import { VM, VMScript } from 'vm2'; -import { omit } from '../../../../lib/utils/omit'; -import * as s from '../../../../lib/utils/stringUtils'; -import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { httpCall } from '../../../../server/lib/http/call'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { settings } from '../../../settings/server'; import { outgoingEvents } from '../../lib/outgoingEvents'; -import { forbiddenModelMethods } from '../api/api'; import { outgoingLogger } from '../logger'; - -const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); +import { IsolatedVMScriptEngine } from './isolated-vm/isolated-vm'; +import { updateHistory } from './updateHistory'; +import { VM2ScriptEngine } from './vm2/vm2'; class RocketChatIntegrationHandler { constructor() { this.successResults = [200, 201, 202]; this.compiledScripts = {}; this.triggers = {}; + this.vm2Engine = new VM2ScriptEngine(false); + this.ivmEngine = new IsolatedVMScriptEngine(false); } addIntegration(record) { @@ -51,6 +46,10 @@ class RocketChatIntegrationHandler { } } + getEngine(integration) { + return integration.scriptEngine === 'isolated-vm' ? this.ivmEngine : this.vm2Engine; + } + removeIntegration(record) { for (const trigger of Object.values(this.triggers)) { delete trigger[record._id]; @@ -67,114 +66,6 @@ class RocketChatIntegrationHandler { return false; } - async updateHistory({ - historyId, - step, - integration, - event, - data, - triggerWord, - ranPrepareScript, - prepareSentMessage, - processSentMessage, - resultMessage, - finished, - url, - httpCallData, - httpError, - httpResult, - error, - errorStack, - }) { - const history = { - type: 'outgoing-webhook', - step, - }; - - // Usually is only added on initial insert - if (integration) { - history.integration = integration; - } - - // Usually is only added on initial insert - if (event) { - history.event = event; - } - - if (data) { - history.data = { ...data }; - - if (data.user) { - history.data.user = omit(data.user, 'services'); - } - - if (data.room) { - history.data.room = data.room; - } - } - - if (triggerWord) { - history.triggerWord = triggerWord; - } - - if (typeof ranPrepareScript !== 'undefined') { - history.ranPrepareScript = ranPrepareScript; - } - - if (prepareSentMessage) { - history.prepareSentMessage = prepareSentMessage; - } - - if (processSentMessage) { - history.processSentMessage = processSentMessage; - } - - if (resultMessage) { - history.resultMessage = resultMessage; - } - - if (typeof finished !== 'undefined') { - history.finished = finished; - } - - if (url) { - history.url = url; - } - - if (typeof httpCallData !== 'undefined') { - history.httpCallData = httpCallData; - } - - if (httpError) { - history.httpError = httpError; - } - - if (typeof httpResult !== 'undefined') { - history.httpResult = JSON.stringify(httpResult, null, 2); - } - - if (typeof error !== 'undefined') { - history.error = error; - } - - if (typeof errorStack !== 'undefined') { - history.errorStack = errorStack; - } - - if (historyId) { - await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); - return historyId; - } - - history._createdAt = new Date(); - - const _id = Random.id(); - - await IntegrationHistory.insertOne({ _id, ...history }); - - return _id; - } - // Trigger is the trigger, nameOrId is a string which is used to try and find a room, room is a room, message is a message, and data contains "user_name" if trigger.impersonateUser is truthful. async sendMessage({ trigger, nameOrId = '', room, message, data }) { let user; @@ -229,199 +120,6 @@ class RocketChatIntegrationHandler { return message; } - buildSandbox(store = {}) { - const httpAsync = async (method, url, options) => { - try { - return { - result: await httpCall(method, url, options), - }; - } catch (error) { - return { error }; - } - }; - - const sandbox = { - scriptTimeout(reject) { - return setTimeout(() => reject('timed out'), 3000); - }, - _, - s, - console, - moment, - Promise, - Store: { - set: (key, val) => { - store[key] = val; - }, - get: (key) => store[key], - }, - HTTP: (method, url, options) => { - // TODO: deprecate, track and alert - return deasyncPromise(httpAsync(method, url, options)); - }, - // TODO: Export fetch as the non deprecated method - }; - - Object.keys(Models) - .filter((k) => !forbiddenModelMethods.includes(k)) - .forEach((k) => { - sandbox[k] = Models[k]; - }); - - return { store, sandbox }; - } - - getIntegrationScript(integration) { - if (DISABLE_INTEGRATION_SCRIPTS) { - throw new Meteor.Error('integration-scripts-disabled'); - } - - const compiledScript = this.compiledScripts[integration._id]; - if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { - return compiledScript.script; - } - - const script = integration.scriptCompiled; - const { store, sandbox } = this.buildSandbox(); - - try { - outgoingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); - outgoingLogger.debug(script); - - const vmScript = new VMScript(`${script}; Script;`, 'script.js'); - const vm = new VM({ - sandbox, - }); - - const ScriptClass = vm.run(vmScript); - - if (ScriptClass) { - this.compiledScripts[integration._id] = { - script: new ScriptClass(), - store, - _updatedAt: integration._updatedAt, - }; - - return this.compiledScripts[integration._id].script; - } - } catch (err) { - outgoingLogger.error({ - msg: 'Error evaluating Script in Trigger', - integration: integration.name, - script, - err, - }); - throw new Meteor.Error('error-evaluating-script'); - } - - outgoingLogger.error(`Class "Script" not in Trigger ${integration.name}:`); - throw new Meteor.Error('class-script-not-found'); - } - - hasScriptAndMethod(integration, method) { - if ( - DISABLE_INTEGRATION_SCRIPTS || - integration.scriptEnabled !== true || - !integration.scriptCompiled || - integration.scriptCompiled.trim() === '' - ) { - return false; - } - - let script; - try { - script = this.getIntegrationScript(integration); - } catch (e) { - return false; - } - - return typeof script[method] !== 'undefined'; - } - - async executeScript(integration, method, params, historyId) { - if (DISABLE_INTEGRATION_SCRIPTS) { - return; - } - - let script; - try { - script = this.getIntegrationScript(integration); - } catch (e) { - await this.updateHistory({ - historyId, - step: 'execute-script-getting-script', - error: true, - errorStack: e, - }); - return; - } - - if (!script[method]) { - outgoingLogger.error(`Method "${method}" no found in the Integration "${integration.name}"`); - await this.updateHistory({ historyId, step: `execute-script-no-method-${method}` }); - return; - } - - try { - const { sandbox } = this.buildSandbox(this.compiledScripts[integration._id].store); - sandbox.script = script; - sandbox.method = method; - sandbox.params = params; - - await this.updateHistory({ historyId, step: `execute-script-before-running-${method}` }); - - const vm = new VM({ - timeout: 3000, - sandbox, - }); - - const result = await new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - const scriptResult = await vm.run(` - new Promise((resolve, reject) => { - scriptTimeout(reject); - try { - resolve(script[method](params)) - } catch(e) { - reject(e); - } - }).catch((error) => { throw new Error(error); }); - `); - - resolve(scriptResult); - } catch (e) { - reject(e); - } - }); - }); - - outgoingLogger.debug({ - msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, - result, - }); - - return result; - } catch (err) { - await this.updateHistory({ - historyId, - step: `execute-script-error-running-${method}`, - error: true, - errorStack: err.stack.replace(/^/gm, ' '), - }); - outgoingLogger.error({ - msg: 'Error running Script in the Integration', - integration: integration.name, - err, - }); - outgoingLogger.debug({ - msg: 'Error running Script in the Integration', - integration: integration.name, - script: integration.scriptCompiled, - }); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. - } - } - eventNameArgumentsToObject(...args) { const argObject = { event: args[0], @@ -680,6 +378,17 @@ class RocketChatIntegrationHandler { } } + // Ensure that any errors thrown by the script engine will contibue to be compatible with Meteor.Error + async wrapScriptEngineCall(getter) { + return wrapExceptions(getter).catch((error) => { + if (error instanceof Error) { + throw new Meteor.Error(error.message); + } + + throw error; + }); + } + async executeTriggerUrl(url, trigger, { event, message, room, owner, user }, theHistoryId, tries = 0) { if (!this.isTriggerEnabled(trigger)) { outgoingLogger.warn(`The trigger "${trigger.name}" is no longer enabled, stopping execution of it at try: ${tries}`); @@ -715,7 +424,7 @@ class RocketChatIntegrationHandler { return; } - const historyId = await this.updateHistory({ + const historyId = await updateHistory({ step: 'start-execute-trigger-url', integration: trigger, event, @@ -731,36 +440,32 @@ class RocketChatIntegrationHandler { } this.mapEventArgsToData(data, { trigger, event, message, room, owner, user }); - await this.updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); + await updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); outgoingLogger.info(`Will be executing the Integration "${trigger.name}" to the url: ${url}`); outgoingLogger.debug({ data }); - let opts = { - params: {}, - method: 'POST', - url, - data, - auth: undefined, - headers: { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', - }, - }; + const scriptEngine = this.getEngine(trigger); - if (this.hasScriptAndMethod(trigger, 'prepare_outgoing_request')) { - opts = await this.executeScript(trigger, 'prepare_outgoing_request', { request: opts }, historyId); - } + const opts = await this.wrapScriptEngineCall(() => + scriptEngine.prepareOutgoingRequest({ + integration: trigger, + data, + url, + historyId, + }), + ); - await this.updateHistory({ historyId, step: 'after-maybe-ran-prepare', ranPrepareScript: true }); + await updateHistory({ historyId, step: 'after-maybe-ran-prepare', ranPrepareScript: true }); if (!opts) { - await this.updateHistory({ historyId, step: 'after-prepare-no-opts', finished: true }); + await updateHistory({ historyId, step: 'after-prepare-no-opts', finished: true }); return; } if (opts.message) { const prepareMessage = await this.sendMessage({ trigger, room, message: opts.message, data }); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-prepare-send-message', prepareSentMessage: prepareMessage, @@ -768,7 +473,7 @@ class RocketChatIntegrationHandler { } if (!opts.url || !opts.method) { - await this.updateHistory({ historyId, step: 'after-prepare-no-url_or_method', finished: true }); + await updateHistory({ historyId, step: 'after-prepare-no-url_or_method', finished: true }); return; } @@ -782,7 +487,7 @@ class RocketChatIntegrationHandler { opts.headers.Authorization = `Basic ${base64}`; } - await this.updateHistory({ + await updateHistory({ historyId, step: 'pre-http-call', url: opts.url, @@ -823,47 +528,42 @@ class RocketChatIntegrationHandler { } })(); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-http-call', httpError: null, httpResult: content, }); - if (this.hasScriptAndMethod(trigger, 'process_outgoing_response')) { - const sandbox = { + const responseContent = await this.wrapScriptEngineCall(() => + scriptEngine.processOutgoingResponse({ + integration: trigger, request: opts, - response: { - error: null, - status_code: res.status, // These values will be undefined to close issues #4175, #5762, and #5896 - content, - content_raw: content, - headers: Object.fromEntries(res.headers), - }, - }; - - const scriptResult = await this.executeScript(trigger, 'process_outgoing_response', sandbox, historyId); - - if (scriptResult && scriptResult.content) { - const resultMessage = await this.sendMessage({ - trigger, - room, - message: scriptResult.content, - data, - }); - await this.updateHistory({ - historyId, - step: 'after-process-send-message', - processSentMessage: resultMessage, - finished: true, - }); - return; - } + response: res, + content, + historyId, + }), + ); + + if (responseContent) { + const resultMessage = await this.sendMessage({ + trigger, + room, + message: responseContent, + data, + }); + await updateHistory({ + historyId, + step: 'after-process-send-message', + processSentMessage: resultMessage, + finished: true, + }); + return; + } - if (scriptResult === false) { - await this.updateHistory({ historyId, step: 'after-process-false-result', finished: true }); - return; - } + if (responseContent === false) { + await updateHistory({ historyId, step: 'after-process-false-result', finished: true }); + return; } // if the result contained nothing or wasn't a successful statusCode @@ -875,14 +575,14 @@ class RocketChatIntegrationHandler { }); if (res.status === 410) { - await this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); + await updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); await Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } }); return; } if (res.status === 500) { - await this.updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); + await updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); outgoingLogger.error({ msg: `Error "500" for the Integration "${trigger.name}" to ${url}.`, content, @@ -893,7 +593,7 @@ class RocketChatIntegrationHandler { if (trigger.retryFailedCalls) { if (tries < trigger.retryCount && trigger.retryDelay) { - await this.updateHistory({ historyId, error: true, step: `going-to-retry-${tries + 1}` }); + await updateHistory({ historyId, error: true, step: `going-to-retry-${tries + 1}` }); let waitTime; @@ -912,7 +612,7 @@ class RocketChatIntegrationHandler { break; default: const er = new Error("The integration's retryDelay setting is invalid."); - await this.updateHistory({ + await updateHistory({ historyId, step: 'failed-and-retry-delay-is-invalid', error: true, @@ -926,10 +626,10 @@ class RocketChatIntegrationHandler { void this.executeTriggerUrl(url, trigger, { event, message, room, owner, user }, historyId, tries + 1); }, waitTime); } else { - await this.updateHistory({ historyId, step: 'too-many-retries', error: true }); + await updateHistory({ historyId, step: 'too-many-retries', error: true }); } } else { - await this.updateHistory({ + await updateHistory({ historyId, step: 'failed-and-not-configured-to-retry', error: true, @@ -943,7 +643,7 @@ class RocketChatIntegrationHandler { if (content && this.successResults.includes(res.status)) { if (data?.text || data?.attachments) { const resultMsg = await this.sendMessage({ trigger, room, message: data, data }); - await this.updateHistory({ + await updateHistory({ historyId, step: 'url-response-sent-message', resultMessage: resultMsg, @@ -954,7 +654,7 @@ class RocketChatIntegrationHandler { }) .catch(async (error) => { outgoingLogger.error(error); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-http-call', httpError: error, diff --git a/apps/meteor/app/integrations/server/lib/updateHistory.ts b/apps/meteor/app/integrations/server/lib/updateHistory.ts new file mode 100644 index 000000000000..9f7a3017108d --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/updateHistory.ts @@ -0,0 +1,96 @@ +import type { IIntegrationHistory, IIntegration, IMessage, AtLeast } from '@rocket.chat/core-typings'; +import { IntegrationHistory } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; + +import { omit } from '../../../../lib/utils/omit'; + +export const updateHistory = async ({ + historyId, + step, + integration, + event, + data, + triggerWord, + ranPrepareScript, + prepareSentMessage, + processSentMessage, + resultMessage, + finished, + url, + httpCallData, + httpError, + httpResult, + error, + errorStack, +}: { + historyId: IIntegrationHistory['_id']; + step: IIntegrationHistory['step']; + integration?: IIntegration; + event?: string; + triggerWord?: string; + ranPrepareScript?: boolean; + prepareSentMessage?: { channel: string; message: Partial }[]; + processSentMessage?: { channel: string; message: Partial }[]; + resultMessage?: { channel: string; message: Partial }[]; + finished?: boolean; + url?: string; + httpCallData?: Record; // ProcessedOutgoingRequest.data + httpError?: any; // null or whatever error type `fetch` may throw + httpResult?: string | null; + + error?: boolean; + errorStack?: any; // Error | Error['stack'] + + data?: Record; +}) => { + const { user: userData, room: roomData, ...fullData } = data || {}; + + const history: AtLeast = { + type: 'outgoing-webhook', + step, + + // Usually is only added on initial insert + ...(integration ? { integration } : {}), + // Usually is only added on initial insert + ...(event ? { event } : {}), + ...(fullData + ? { + data: { + ...fullData, + ...(userData ? { user: omit(userData, 'services') } : {}), + ...(roomData ? { room: roomData } : {}), + }, + } + : {}), + ...(triggerWord ? { triggerWord } : {}), + ...(typeof ranPrepareScript !== 'undefined' ? { ranPrepareScript } : {}), + ...(prepareSentMessage ? { prepareSentMessage } : {}), + ...(processSentMessage ? { processSentMessage } : {}), + ...(resultMessage ? { resultMessage } : {}), + ...(typeof finished !== 'undefined' ? { finished } : {}), + ...(url ? { url } : {}), + ...(typeof httpCallData !== 'undefined' ? { httpCallData } : {}), + ...(httpError ? { httpError } : {}), + ...(typeof httpResult !== 'undefined' ? { httpResult: JSON.stringify(httpResult, null, 2) } : {}), + ...(typeof error !== 'undefined' ? { error } : {}), + ...(typeof errorStack !== 'undefined' ? { errorStack } : {}), + }; + + if (historyId) { + await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); + return historyId; + } + + // Can't create a new history without there being an integration + if (!history.integration) { + throw new Error('error-invalid-integration'); + } + + history._createdAt = new Date(); + + const _id = Random.id(); + + await IntegrationHistory.insertOne({ _id, ...history } as IIntegrationHistory); + + return _id; +}; diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index d9c2db78b62e..398f81161279 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -1,19 +1,18 @@ import type { IUser, INewOutgoingIntegration, IOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import { pick } from '@rocket.chat/tools'; import { Babel } from 'meteor/babel-compiler'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { outgoingEvents } from '../../lib/outgoingEvents'; +import { isScriptEngineFrozen } from './validateScriptEngine'; const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages']; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - function _verifyRequiredFields(integration: INewOutgoingIntegration | IUpdateOutgoingIntegration): void { if ( !integration.event || @@ -152,6 +151,7 @@ export const validateOutgoingIntegration = async function ( const integrationData: IOutgoingIntegration = { ...integration, + scriptEngine: integration.scriptEngine ?? 'isolated-vm', type: 'webhook-outgoing', channel: channels, userId: user._id, @@ -171,7 +171,13 @@ export const validateOutgoingIntegration = async function ( delete integrationData.triggerWords; } - if (!FREEZE_INTEGRATION_SCRIPTS && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + // Only compile the script if it is enabled and using a sandbox that is not frozen + if ( + !isScriptEngineFrozen(integrationData.scriptEngine) && + integration.scriptEnabled === true && + integration.script && + integration.script.trim() !== '' + ) { try { const babelOptions = Object.assign(Babel.getDefaultOptions({ runtime: false }), { compact: true, @@ -183,7 +189,7 @@ export const validateOutgoingIntegration = async function ( integrationData.scriptError = undefined; } catch (e) { integrationData.scriptCompiled = undefined; - integrationData.scriptError = e instanceof Error ? _.pick(e, 'name', 'message', 'stack') : undefined; + integrationData.scriptError = e instanceof Error ? pick(e, 'name', 'message', 'stack') : undefined; } } diff --git a/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts b/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts new file mode 100644 index 000000000000..c20dc9c59427 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts @@ -0,0 +1,26 @@ +import type { IntegrationScriptEngine } from '@rocket.chat/core-typings'; +import { wrapExceptions } from '@rocket.chat/tools'; + +const FREEZE_INTEGRATION_SCRIPTS_VALUE = String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase(); +const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(FREEZE_INTEGRATION_SCRIPTS_VALUE); + +export const validateScriptEngine = (engine?: IntegrationScriptEngine) => { + if (FREEZE_INTEGRATION_SCRIPTS) { + throw new Error('integration-scripts-disabled'); + } + + const engineCode = engine === 'isolated-vm' ? 'ivm' : 'vm2'; + + if (engineCode === FREEZE_INTEGRATION_SCRIPTS_VALUE) { + if (engineCode === 'ivm') { + throw new Error('integration-scripts-isolated-vm-disabled'); + } + + throw new Error('integration-scripts-vm2-disabled'); + } + + return true; +}; + +export const isScriptEngineFrozen = (engine?: IntegrationScriptEngine) => + wrapExceptions(() => !validateScriptEngine(engine)).catch(() => true); diff --git a/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts b/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts new file mode 100644 index 000000000000..9ba74404cf26 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts @@ -0,0 +1,88 @@ +import * as Models from '@rocket.chat/models'; +import moment from 'moment'; +import _ from 'underscore'; + +import * as s from '../../../../../lib/utils/stringUtils'; +import { deasyncPromise } from '../../../../../server/deasync/deasync'; +import { httpCall } from '../../../../../server/lib/http/call'; + +const forbiddenModelMethods: readonly (keyof typeof Models)[] = ['registerModel', 'getCollectionName']; + +type ModelName = Exclude; + +export type Vm2Sandbox = { + scriptTimeout: (reject: (reason?: any) => void) => ReturnType; + _: typeof _; + s: typeof s; + console: typeof console; + moment: typeof moment; + Promise: typeof Promise; + Store: { + set: IsIncoming extends true ? (key: string, value: any) => any : (key: string, value: any) => void; + get: (key: string) => any; + }; + HTTP: (method: string, url: string, options: Record) => unknown; +} & (IsIncoming extends true ? { Livechat: undefined } : never) & + Record; + +export const buildSandbox = ( + store: Record, + isIncoming?: IsIncoming, +): { + store: Record; + sandbox: Vm2Sandbox; +} => { + const httpAsync = async (method: string, url: string, options: Record) => { + try { + return { + result: await httpCall(method, url, options), + }; + } catch (error) { + return { error }; + } + }; + + const sandbox = { + scriptTimeout(reject: (reason?: any) => void) { + return setTimeout(() => reject('timed out'), 3000); + }, + _, + s, + console, + moment, + Promise, + // There's a small difference between the sandbox that is sent to incoming and to outgoing scripts + // Technically we could unify this but since we're deprecating vm2 anyway I'm keeping this old behavior here until the feature is removed completely + ...(isIncoming + ? { + Livechat: undefined, + Store: { + set: (key: string, val: any): any => { + store[key] = val; + return val; + }, + get: (key: string) => store[key], + }, + } + : { + Store: { + set: (key: string, val: any): void => { + store[key] = val; + }, + get: (key: string) => store[key], + }, + }), + HTTP: (method: string, url: string, options: Record) => { + // TODO: deprecate, track and alert + return deasyncPromise(httpAsync(method, url, options)); + }, + } as Vm2Sandbox; + + (Object.keys(Models) as ModelName[]) + .filter((k) => !forbiddenModelMethods.includes(k)) + .forEach((k) => { + sandbox[k] = Models[k]; + }); + + return { store, sandbox }; +}; diff --git a/apps/meteor/app/integrations/server/lib/vm2/vm2.ts b/apps/meteor/app/integrations/server/lib/vm2/vm2.ts new file mode 100644 index 000000000000..5f7519d69346 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/vm2/vm2.ts @@ -0,0 +1,111 @@ +import type { IIntegration } from '@rocket.chat/core-typings'; +import { VM, VMScript } from 'vm2'; + +import { IntegrationScriptEngine } from '../ScriptEngine'; +import type { IScriptClass } from '../definition'; +import { buildSandbox, type Vm2Sandbox } from './buildSandbox'; + +const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true', 'vm2'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); + +export class VM2ScriptEngine extends IntegrationScriptEngine { + protected isDisabled(): boolean { + return DISABLE_INTEGRATION_SCRIPTS; + } + + protected buildSandbox(store: Record = {}): { store: Record; sandbox: Vm2Sandbox } { + return buildSandbox(store, this.incoming); + } + + protected async runScriptMethod({ + integrationId, + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: IScriptClass; + method: keyof IScriptClass; + params: Record; + }): Promise { + const { sandbox } = this.buildSandbox(this.compiledScripts[integrationId].store); + + const vm = new VM({ + timeout: 3000, + sandbox: { + ...sandbox, + script, + method, + params, + ...(this.incoming && 'request' in params ? { request: params.request } : {}), + }, + }); + + return new Promise((resolve, reject) => { + process.nextTick(async () => { + try { + const scriptResult = await vm.run(` + new Promise((resolve, reject) => { + scriptTimeout(reject); + try { + resolve(script[method](params)) + } catch(e) { + reject(e); + } + }).catch((error) => { throw new Error(error); }); + `); + + resolve(scriptResult); + } catch (e) { + reject(e); + } + }); + }); + } + + protected async getIntegrationScript(integration: IIntegration): Promise> { + if (this.disabled) { + throw new Error('integration-scripts-disabled'); + } + + const compiledScript = this.compiledScripts[integration._id]; + if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { + return compiledScript.script; + } + + const script = integration.scriptCompiled; + const { store, sandbox } = this.buildSandbox(); + + try { + this.logger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); + this.logger.debug(script); + + const vmScript = new VMScript(`${script}; Script;`, 'script.js'); + const vm = new VM({ + sandbox, + }); + + const ScriptClass = vm.run(vmScript); + + if (ScriptClass) { + this.compiledScripts[integration._id] = { + script: new ScriptClass(), + store, + _updatedAt: integration._updatedAt, + }; + + return this.compiledScripts[integration._id].script; + } + } catch (err) { + this.logger.error({ + msg: 'Error evaluating Script in Trigger', + integration: integration.name, + script, + err, + }); + throw new Error('error-evaluating-script'); + } + + this.logger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name }); + throw new Error('class-script-not-found'); + } +} diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index bf84957ba8ea..45548a17a565 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -8,11 +8,10 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -32,6 +31,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn alias: Match.Maybe(String), emoji: Match.Maybe(String), scriptEnabled: Boolean, + scriptEngine: Match.Maybe(String), overrideDestinationChannelEnabled: Match.Maybe(Boolean), script: Match.Maybe(String), avatar: Match.Maybe(String), @@ -76,8 +76,8 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn }); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + if (integration.script?.trim()) { + validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } const user = await Users.findOne({ username: integration.username }); @@ -90,6 +90,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn const integrationData: IIncomingIntegration = { ...integration, + scriptEngine: integration.scriptEngine ?? 'isolated-vm', type: 'webhook-incoming', channel: channels, overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false, @@ -99,7 +100,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn _createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }; - if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + // Only compile the script if it is enabled and using a sandbox that is not frozen + if ( + !isScriptEngineFrozen(integrationData.scriptEngine) && + integration.scriptEnabled === true && + integration.script && + integration.script.trim() !== '' + ) { try { let babelOptions = Babel.getDefaultOptions({ runtime: false }); babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index b865c72e0cca..5358e3233ce7 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -1,16 +1,16 @@ import type { IIntegration, INewIncomingIntegration, IUpdateIncomingIntegration } from '@rocket.chat/core-typings'; import { Integrations, Roles, Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Babel } from 'meteor/babel-compiler'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -66,11 +66,20 @@ Meteor.methods({ }); } - if (FREEZE_INTEGRATION_SCRIPTS) { - if (currentIntegration.script?.trim() !== integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); - } - } else { + const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2'; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } + + const isFrozen = isScriptEngineFrozen(scriptEngine); + + if (!isFrozen) { let scriptCompiled: string | undefined; let scriptError: Pick | undefined; @@ -165,11 +174,12 @@ Meteor.methods({ emoji: integration.emoji, alias: integration.alias, channel: channels, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { script: integration.script, scriptEnabled: integration.scriptEnabled, + scriptEngine, }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 9e5d29261b36..59879f99d475 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; +import { validateScriptEngine } from '../../lib/validateScriptEngine'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,8 +15,6 @@ declare module '@rocket.chat/ui-contexts' { } } -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - export const addOutgoingIntegration = async (userId: string, integration: INewOutgoingIntegration): Promise => { check( integration, @@ -29,6 +28,7 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu emoji: Match.Maybe(String), scriptEnabled: Boolean, script: Match.Maybe(String), + scriptEngine: Match.Maybe(String), urls: Match.Maybe([String]), event: Match.Maybe(String), triggerWords: Match.Maybe([String]), @@ -52,8 +52,8 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu throw new Meteor.Error('not_authorized'); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + if (integration.script?.trim()) { + validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } const integrationData = await validateOutgoingIntegration(integration, userId); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 166badee823d..9e62561ebf9a 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -1,10 +1,12 @@ import type { IIntegration, INewOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; import { Integrations, Users } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; +import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,8 +18,6 @@ declare module '@rocket.chat/ui-contexts' { } } -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - Meteor.methods({ async updateOutgoingIntegration(integrationId, _integration) { if (!this.userId) { @@ -53,10 +53,19 @@ Meteor.methods({ throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim() !== currentIntegration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2'; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); } + const isFrozen = isScriptEngineFrozen(scriptEngine); + await Integrations.updateOne( { _id: integrationId }, { @@ -74,11 +83,12 @@ Meteor.methods({ userId: integration.userId, urls: integration.urls, token: integration.token, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { script: integration.script, scriptEnabled: integration.scriptEnabled, + scriptEngine, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), triggerWords: integration.triggerWords, @@ -90,7 +100,7 @@ Meteor.methods({ _updatedAt: new Date(), _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), }, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { $unset: { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 6dc477a2926f..835f59419ad5 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -3,6 +3,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { await callbacks.run('beforeJoinDefaultChannels', user); @@ -11,6 +12,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: }).toArray(); for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); // Add a subscription to this user await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), @@ -20,6 +22,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: userMentions: 1, groupMentions: 0, ...(room.favorite && { f: true }), + ...autoTranslateConfig, }); // Insert user joined message diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 660af823de9e..4e29576cf3bb 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; export const addUserToRoom = async function ( @@ -24,7 +25,7 @@ export const addUserToRoom = async function ( }); } - const userToBeAdded = typeof user !== 'string' ? user : await Users.findOneByUsername(user.replace('@', '')); + const userToBeAdded = typeof user === 'string' ? await Users.findOneByUsername(user.replace('@', '')) : await Users.findOneById(user._id); const roomDirectives = roomCoordinator.getRoomDirectives(room.t); if (!userToBeAdded) { @@ -70,6 +71,8 @@ export const addUserToRoom = async function ( await callbacks.run('beforeJoinRoom', userToBeAdded, room); } + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded); + await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, open: true, @@ -77,6 +80,7 @@ export const addUserToRoom = async function ( unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); if (!userToBeAdded.username) { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 192139f96b7c..312451f54845 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; @@ -8,7 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { createDirectRoom } from './createDirectRoom'; @@ -19,10 +20,90 @@ const isValidName = (name: unknown): name is string => { const onlyUsernames = (members: unknown): members is string[] => Array.isArray(members) && members.every((member) => typeof member === 'string'); +async function createUsersSubscriptions({ + room, + shouldBeHandledByFederation, + members, + now, + owner, + options, +}: { + room: IRoom; + shouldBeHandledByFederation: boolean; + members: string[]; + now: Date; + owner: IUser; + options?: ICreateRoomParams['options']; +}) { + if (shouldBeHandledByFederation) { + const extra: Partial = options?.subscriptionExtra || {}; + extra.open = true; + extra.ls = now; + + if (room.prid) { + extra.prid = room.prid; + } + + await Subscriptions.createWithRoomAndUser(room, owner, extra); + + return; + } + + const subs = []; + + const memberIds = []; + + const membersCursor = Users.findUsersByUsernames>(members, { + projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 }, + }); + + for await (const member of membersCursor) { + try { + await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room); + await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner }); + } catch (error) { + continue; + } + + memberIds.push(member._id); + + const extra: Partial = options?.subscriptionExtra || {}; + + extra.open = true; + + if (room.prid) { + extra.prid = room.prid; + } + + if (member.username === owner.username) { + extra.ls = now; + extra.roles = ['owner']; + } + + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(member); + + subs.push({ + user: member, + extraData: { + ...extra, + ...autoTranslateConfig, + }, + }); + } + + if (!['d', 'l'].includes(room.t)) { + await Users.addRoomByUserIds(memberIds, room._id); + } + + await Subscriptions.createWithRoomAndManyUsers(room, subs); + + await Rooms.incUsersCountById(room._id, subs.length); +} + export const createRoom = async ( type: T, name: T extends 'd' ? undefined : string, - ownerUsername: string | undefined, + owner: T extends 'd' ? IUser | undefined : IUser, members: T extends 'd' ? IUser[] : string[] = [], excludeSelf?: boolean, readOnly?: boolean, @@ -45,7 +126,7 @@ export const createRoom = async ( // options, }); if (type === 'd') { - return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername }); + return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username }); } if (!onlyUsernames(members)) { @@ -61,15 +142,13 @@ export const createRoom = async ( }); } - if (!ownerUsername) { + if (!owner) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom', }); } - const owner = await Users.findOneByUsernameIgnoringCase(ownerUsername, { projection: { username: 1, name: 1 } }); - - if (!ownerUsername || !owner) { + if (!owner?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom', }); @@ -138,51 +217,12 @@ export const createRoom = async ( if (type === 'c') { await callbacks.run('beforeCreateChannel', owner, roomProps); } - const room = await Rooms.createWithFullRoomData(roomProps); - const shouldBeHandledByFederation = room.federated === true || ownerUsername.includes(':'); - if (shouldBeHandledByFederation) { - const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; - extra.ls = now; - - if (room.prid) { - extra.prid = room.prid; - } - - await Subscriptions.createWithRoomAndUser(room, owner, extra); - } else { - for await (const username of [...new Set(members)]) { - const member = await Users.findOneByUsername(username, { - projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 }, - }); - if (!member) { - continue; - } - - try { - await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room); - await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner }); - } catch (error) { - continue; - } - - const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; - - if (room.prid) { - extra.prid = room.prid; - } - - if (username === owner.username) { - extra.ls = now; - } + const room = await Rooms.createWithFullRoomData(roomProps); - await Subscriptions.createWithRoomAndUser(room, member, extra); - } - } + const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':'); - await addUserRolesAsync(owner._id, ['owner'], room._id); + await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); if (type === 'c') { if (room.teamId) { @@ -191,7 +231,7 @@ export const createRoom = async ( await Message.saveSystemMessage('user-added-room-to-team', team.roomId, room.name || '', owner); } } - await callbacks.run('afterCreateChannel', owner, room); + callbacks.runAsync('afterCreateChannel', owner, room); } else if (type === 'p') { callbacks.runAsync('afterCreatePrivateGroup', owner, room); } diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts index 934014b794a1..11e4418c4510 100644 --- a/apps/meteor/app/lib/server/functions/notifications/index.ts +++ b/apps/meteor/app/lib/server/functions/notifications/index.ts @@ -5,7 +5,6 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { settings } from '../../../../settings/server'; -import { joinRoomMethod } from '../../methods/joinRoom'; /** * This function returns a string ready to be shown in the notification @@ -66,7 +65,3 @@ export function messageContainsHighlight(message: IMessage, highlights: string[] return regexp.test(message.msg); }); } - -export async function callJoinRoom(userId: string, rid: string): Promise { - await joinRoomMethod(userId, rid); -} diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index f912626c833e..42438be4ab7a 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -15,6 +15,7 @@ import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { validateEmailDomain } from '../lib'; +import { generatePassword } from '../lib/generatePassword'; import { passwordPolicy } from '../lib/passwordPolicy'; import { checkEmailAvailability } from './checkEmailAvailability'; import { checkUsernameAvailability } from './checkUsernameAvailability'; @@ -344,7 +345,7 @@ export const saveUser = async function (userId, userData) { if (userData.hasOwnProperty('setRandomPassword')) { if (userData.setRandomPassword) { - userData.password = passwordPolicy.generatePassword(); + userData.password = generatePassword(); userData.requirePasswordChange = true; sendPassword = true; } diff --git a/apps/meteor/app/lib/server/functions/sendMessage.js b/apps/meteor/app/lib/server/functions/sendMessage.js index a1399b5b19e9..72247d4b1870 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.js +++ b/apps/meteor/app/lib/server/functions/sendMessage.js @@ -1,3 +1,4 @@ +import { Message } from '@rocket.chat/core-services'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -247,6 +248,8 @@ export const sendMessage = async function (user, message, room, upsert = false, parseUrlsInMessage(message, previewUrls); + message = await Message.beforeSave({ message, room, user }); + message = await callbacks.run('beforeSaveMessage', message, room); if (message) { if (message.t === 'otr') { diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 05c17906374e..9c544bd9a333 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -1,3 +1,4 @@ +import { Message } from '@rocket.chat/core-services'; import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -48,6 +49,14 @@ export const updateMessage = async function ( parseUrlsInMessage(message, previewUrls); + const room = await Rooms.findOneById(message.rid); + if (!room) { + return; + } + + // TODO remove type cast + message = await Message.beforeSave({ message: message as IMessage, room, user }); + message = await callbacks.run('beforeSaveMessage', message); const { _id, ...editedMessage } = message; @@ -67,12 +76,6 @@ export const updateMessage = async function ( }, ); - const room = await Rooms.findOneById(message.rid); - - if (!room) { - return; - } - if (Apps?.isLoaded()) { // This returns a promise, but it won't mutate anything about the message // so, we don't really care if it is successful or fails diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts index 597d03752dcc..8fa779ec9644 100644 --- a/apps/meteor/app/lib/server/index.ts +++ b/apps/meteor/app/lib/server/index.ts @@ -23,7 +23,6 @@ import './methods/deleteUserOwnAccount'; import './methods/executeSlashCommandPreview'; import './startup/filterATAllTag'; import './startup/filterATHereTag'; -import './methods/filterBadWords'; import './methods/getChannelHistory'; import './methods/getRoomJoinCode'; import './methods/getRoomRoles'; diff --git a/apps/meteor/app/lib/server/lib/generatePassword.ts b/apps/meteor/app/lib/server/lib/generatePassword.ts new file mode 100644 index 000000000000..bf8d2474b7a7 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/generatePassword.ts @@ -0,0 +1,31 @@ +import generator from 'generate-password'; + +import { passwordPolicy } from './passwordPolicy'; + +export const generatePassword = (): string => { + const policies = passwordPolicy.getPasswordPolicy(); + + const maxLength: number = (policies.policy.find(([key]) => key === 'get-password-policy-maxLength')?.[1]?.maxLength as number) || -1; + const minLength: number = (policies.policy.find(([key]) => key === 'get-password-policy-minLength')?.[1]?.minLength as number) || -1; + + const length = Math.min(Math.max(minLength, 12), maxLength > 0 ? maxLength : Number.MAX_SAFE_INTEGER); + + if (policies.enabled) { + for (let i = 0; i < 10; i++) { + const password = generator.generate({ + length, + ...(policies.policy && { numbers: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneSpecialCharacter') && { symbols: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneLowercase') && { lowercase: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneUppercase') && { uppercase: true }), + strict: true, + }); + + if (passwordPolicy.validate(password)) { + return password; + } + } + } + + return generator.generate({ length: 17 }); +}; diff --git a/apps/meteor/app/lib/server/lib/passwordPolicy.js b/apps/meteor/app/lib/server/lib/passwordPolicy.js deleted file mode 100644 index 57490d1712c1..000000000000 --- a/apps/meteor/app/lib/server/lib/passwordPolicy.js +++ /dev/null @@ -1,32 +0,0 @@ -import { settings } from '../../../settings/server'; -import PasswordPolicy from './PasswordPolicyClass'; - -export const passwordPolicy = new PasswordPolicy(); - -settings.watch('Accounts_Password_Policy_Enabled', (value) => { - passwordPolicy.enabled = value; -}); -settings.watch('Accounts_Password_Policy_MinLength', (value) => { - passwordPolicy.minLength = value; -}); -settings.watch('Accounts_Password_Policy_MaxLength', (value) => { - passwordPolicy.maxLength = value; -}); -settings.watch('Accounts_Password_Policy_ForbidRepeatingCharacters', (value) => { - passwordPolicy.forbidRepeatingCharacters = value; -}); -settings.watch('Accounts_Password_Policy_ForbidRepeatingCharactersCount', (value) => { - passwordPolicy.forbidRepeatingCharactersCount = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneLowercase', (value) => { - passwordPolicy.mustContainAtLeastOneLowercase = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneUppercase', (value) => { - passwordPolicy.mustContainAtLeastOneUppercase = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneNumber', (value) => { - passwordPolicy.mustContainAtLeastOneNumber = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneSpecialCharacter', (value) => { - passwordPolicy.mustContainAtLeastOneSpecialCharacter = value; -}); diff --git a/apps/meteor/app/lib/server/lib/passwordPolicy.ts b/apps/meteor/app/lib/server/lib/passwordPolicy.ts new file mode 100644 index 000000000000..b40447ca56ab --- /dev/null +++ b/apps/meteor/app/lib/server/lib/passwordPolicy.ts @@ -0,0 +1,64 @@ +import { PasswordPolicy } from '@rocket.chat/password-policies'; + +import { settings } from '../../../settings/server'; + +const enabled = false; +const minLength = -1; +const maxLength = -1; +const forbidRepeatingCharacters = false; +const forbidRepeatingCharactersCount = 3; +const mustContainAtLeastOneLowercase = false; +const mustContainAtLeastOneUppercase = false; +const mustContainAtLeastOneNumber = false; +const mustContainAtLeastOneSpecialCharacter = false; + +export let passwordPolicy = new PasswordPolicy({ + enabled, + minLength, + maxLength, + forbidRepeatingCharacters, + forbidRepeatingCharactersCount, + mustContainAtLeastOneLowercase, + mustContainAtLeastOneUppercase, + mustContainAtLeastOneNumber, + mustContainAtLeastOneSpecialCharacter, + throwError: true, +}); + +settings.watchMultiple( + [ + 'Accounts_Password_Policy_Enabled', + 'Accounts_Password_Policy_MinLength', + 'Accounts_Password_Policy_MaxLength', + 'Accounts_Password_Policy_ForbidRepeatingCharacters', + 'Accounts_Password_Policy_ForbidRepeatingCharactersCount', + 'Accounts_Password_Policy_AtLeastOneLowercase', + 'Accounts_Password_Policy_AtLeastOneUppercase', + 'Accounts_Password_Policy_AtLeastOneNumber', + 'Accounts_Password_Policy_AtLeastOneSpecialCharacter', + ], + ([ + enabled, + minLength, + maxLength, + forbidRepeatingCharacters, + forbidRepeatingCharactersCount, + mustContainAtLeastOneLowercase, + mustContainAtLeastOneUppercase, + mustContainAtLeastOneNumber, + mustContainAtLeastOneSpecialCharacter, + ]) => { + passwordPolicy = new PasswordPolicy({ + enabled: Boolean(enabled), + minLength: Number(minLength), + maxLength: Number(maxLength), + forbidRepeatingCharacters: Boolean(forbidRepeatingCharacters), + forbidRepeatingCharactersCount: Number(forbidRepeatingCharactersCount), + mustContainAtLeastOneLowercase: Boolean(mustContainAtLeastOneLowercase), + mustContainAtLeastOneUppercase: Boolean(mustContainAtLeastOneUppercase), + mustContainAtLeastOneNumber: Boolean(mustContainAtLeastOneNumber), + mustContainAtLeastOneSpecialCharacter: Boolean(mustContainAtLeastOneSpecialCharacter), + throwError: true, + }); + }, +); diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js index 17296d8f374a..ce262e4e6756 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js @@ -1,3 +1,4 @@ +import { Room } from '@rocket.chat/core-services'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -7,12 +8,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Notification } from '../../../notification-queue/server/NotificationQueue'; import { settings } from '../../../settings/server'; -import { - callJoinRoom, - messageContainsHighlight, - parseMessageTextPerUser, - replaceMentionedUsernamesWithFullNames, -} from '../functions/notifications'; +import { messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications'; import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop'; import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email'; import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobile'; @@ -365,7 +361,7 @@ export async function sendAllNotifications(message, room) { const users = await Promise.all( mentions.map(async (userId) => { - await callJoinRoom(userId, room._id); + await Room.join({ room, user: { _id: userId } }); return userId; }), diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index ff8182cec8c9..98cea517bed4 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -35,8 +35,7 @@ export const createChannelMethod = async ( throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - + const user = await Users.findOneById(userId); if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } @@ -44,7 +43,7 @@ export const createChannelMethod = async ( if (!(await hasPermissionAsync(userId, 'create-c'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); } - return createRoom('c', name, user.username, members, excludeSelf, readOnly, { + return createRoom('c', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index 65298949a345..75097b5c89b8 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -1,4 +1,4 @@ -import type { ICreatedRoom } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; @@ -21,7 +21,7 @@ declare module '@rocket.chat/ui-contexts' { } export const createPrivateGroupMethod = async ( - userId: string, + user: IUser, name: string, members: string[], readOnly = false, @@ -35,23 +35,12 @@ export const createPrivateGroupMethod = async ( > => { check(name, String); check(members, Match.Optional([String])); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - if (!(await hasPermissionAsync(userId, 'create-p'))) { + if (!(await hasPermissionAsync(user._id, 'create-p'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } - return createRoom('p', name, user.username, members, excludeSelf, readOnly, { + return createRoom('p', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); @@ -67,6 +56,13 @@ Meteor.methods({ }); } - return createPrivateGroupMethod(uid, name, members, readOnly, customFields, extraData); + const user = await Users.findOneById(uid); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createPrivateGroup', + }); + } + + return createPrivateGroupMethod(user, name, members, readOnly, customFields, extraData); }, }); diff --git a/apps/meteor/app/lib/server/methods/filterBadWords.ts b/apps/meteor/app/lib/server/methods/filterBadWords.ts deleted file mode 100644 index 3db0ee76e997..000000000000 --- a/apps/meteor/app/lib/server/methods/filterBadWords.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import Filter from 'bad-words'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings/server'; - -const Dep = new Tracker.Dependency(); -Meteor.startup(() => { - settings.watchMultiple(['Message_AllowBadWordsFilter', 'Message_BadWordsFilterList', 'Message_BadWordsWhitelist'], () => { - Dep.changed(); - }); - Tracker.autorun(() => { - Dep.depend(); - const allowBadWordsFilter = settings.get('Message_AllowBadWordsFilter'); - - callbacks.remove('beforeSaveMessage', 'filterBadWords'); - - if (!allowBadWordsFilter) { - return; - } - - const badWordsList = settings.get('Message_BadWordsFilterList') as string | undefined; - const whiteList = settings.get('Message_BadWordsWhitelist') as string | undefined; - - const options = { - list: - badWordsList - ?.split(',') - .map((word) => word.trim()) - .filter(Boolean) || [], - // library definition does not allow optional definition - exclude: undefined, - splitRegex: undefined, - placeHolder: undefined, - regex: undefined, - replaceRegex: undefined, - emptyList: undefined, - }; - - const filter = new Filter(options); - - if (whiteList?.length) { - filter.removeWords(...whiteList.split(',').map((word) => word.trim())); - } - - callbacks.add( - 'beforeSaveMessage', - (message: IMessage) => { - if (!message.msg) { - return message; - } - try { - message.msg = filter.clean(message.msg); - } finally { - // eslint-disable-next-line no-unsafe-finally - return message; - } - }, - callbacks.priority.HIGH, - 'filterBadWords', - ); - }); -}); diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index 355fd49916ae..0fa3ac0b3c3b 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -1,63 +1,31 @@ -import type { IRoom, IRoomWithJoinCode, IUser } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Room } from '@rocket.chat/core-services'; +import type { IRoom } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -import { canAccessRoomAsync } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { addUserToRoom } from '../functions/addUserToRoom'; - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - joinRoom(rid: IRoom['_id'], code?: unknown): boolean | undefined; + joinRoom(rid: IRoom['_id'], code?: string): boolean | undefined; } } -export const joinRoomMethod = async (userId: IUser['_id'], rid: IRoom['_id'], code?: unknown): Promise => { - check(rid, String); - - const user = await Users.findOneById(userId); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); - } - - const room = await Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.JOIN, user._id))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } - if (room.joinCodeRequired === true && code !== room.joinCode && !(await hasPermissionAsync(user._id, 'join-without-join-code'))) { - throw new Meteor.Error('error-code-invalid', 'Invalid Room Password', { - method: 'joinRoom', - }); - } - - return addUserToRoom(rid, user); -}; - Meteor.methods({ async joinRoom(rid, code) { check(rid, String); const userId = await Meteor.userId(); - if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); } - return joinRoomMethod(userId, rid, code); + const room = await Rooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); + } + + return Room.join({ room, user: { _id: userId }, ...(code ? { joinCode: code } : {}) }); }, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js index 6521c0f662dd..7ecb3b3fc100 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.js +++ b/apps/meteor/app/livechat/imports/server/rest/sms.js @@ -56,7 +56,7 @@ const defineVisitor = async (smsNumber, targetDepartment) => { } const id = await LivechatTyped.registerGuest(data); - return LivechatVisitors.findOneById(id); + return LivechatVisitors.findOneEnabledById(id); }; const normalizeLocationSharing = (payload) => { diff --git a/apps/meteor/app/livechat/imports/server/rest/triggers.ts b/apps/meteor/app/livechat/imports/server/rest/triggers.ts index a12d4a988281..a7660827b0ce 100644 --- a/apps/meteor/app/livechat/imports/server/rest/triggers.ts +++ b/apps/meteor/app/livechat/imports/server/rest/triggers.ts @@ -3,7 +3,7 @@ import { isGETLivechatTriggersParams, isPOSTLivechatTriggersParams } from '@rock import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { findTriggers, findTriggerById } from '../../../server/api/lib/triggers'; +import { findTriggers, findTriggerById, deleteTrigger } from '../../../server/api/lib/triggers'; API.v1.addRoute( 'livechat/triggers', @@ -57,5 +57,12 @@ API.v1.addRoute( trigger, }); }, + async delete() { + await deleteTrigger({ + triggerId: this.urlParams._id, + }); + + return API.v1.success(); + }, }, ); diff --git a/apps/meteor/app/livechat/server/api/lib/triggers.ts b/apps/meteor/app/livechat/server/api/lib/triggers.ts index dbb6f8a6633a..4cbafcb0dc73 100644 --- a/apps/meteor/app/livechat/server/api/lib/triggers.ts +++ b/apps/meteor/app/livechat/server/api/lib/triggers.ts @@ -29,3 +29,7 @@ export async function findTriggers({ export async function findTriggerById({ triggerId }: { triggerId: string }): Promise { return LivechatTrigger.findOneById(triggerId); } + +export async function deleteTrigger({ triggerId }: { triggerId: string }): Promise { + await LivechatTrigger.removeById(triggerId); +} diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.ts b/apps/meteor/app/livechat/server/api/lib/visitors.ts index e559aecc892e..0abed5197d78 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.ts +++ b/apps/meteor/app/livechat/server/api/lib/visitors.ts @@ -6,7 +6,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { - const visitor = await LivechatVisitors.findOneById(visitorId); + const visitor = await LivechatVisitors.findOneEnabledById(visitorId); if (!visitor) { throw new Error('visitor-not-found'); } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 517acf33f137..57c1d117f1b0 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -33,7 +33,7 @@ API.v1.addRoute( contactId: String, }); - const contact = await LivechatVisitors.findOneById(this.queryParams.contactId); + const contact = await LivechatVisitors.findOneEnabledById(this.queryParams.contactId); return API.v1.success({ contact }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 2b6f4c00af53..104e2ece94d5 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -269,7 +269,7 @@ API.v1.addRoute( guest.connectionData = normalizeHttpHeaderData(this.request.headers); const visitorId = await LivechatTyped.registerGuest(guest); - visitor = await LivechatVisitors.findOneById(visitorId); + visitor = await LivechatVisitors.findOneEnabledById(visitorId); } const sentMessages = await Promise.all( diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 0fe60248bfba..86629e636bf8 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -326,7 +326,7 @@ API.v1.addRoute( throw new Error('This_conversation_is_already_closed'); } - const guest = await LivechatVisitors.findOneById(room.v?._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); const transferedBy = this.user satisfies TransferByData; transferData.transferredBy = normalizeTransferredByData(transferedBy, room); if (transferData.userId) { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 012b412639ea..ae9d1ea4fd83 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -45,7 +45,7 @@ API.v1.addRoute('livechat/visitor', { const visitorId = await LivechatTyped.registerGuest(guest); - let visitor = await VisitorsRaw.findOneById(visitorId, {}); + let visitor = 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 @@ -65,7 +65,7 @@ API.v1.addRoute('livechat/visitor', { } } - visitor = await VisitorsRaw.findOneById(visitorId, {}); + visitor = await VisitorsRaw.findOneEnabledById(visitorId, {}); } if (!visitor) { @@ -122,7 +122,7 @@ API.v1.addRoute('livechat/visitor/:token', { const { _id } = visitor; const result = await Livechat.removeGuest(_id); - if (!result) { + if (!result.modifiedCount) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } diff --git a/apps/meteor/app/livechat/server/api/v1/webhooks.ts b/apps/meteor/app/livechat/server/api/v1/webhooks.ts index dceb19ed0420..e282e2bd548b 100644 --- a/apps/meteor/app/livechat/server/api/v1/webhooks.ts +++ b/apps/meteor/app/livechat/server/api/v1/webhooks.ts @@ -66,7 +66,7 @@ API.v1.addRoute( const webhookUrl = settings.get('Livechat_webhookUrl'); if (!webhookUrl) { - return API.v1.failure('Webhook URL is not set'); + return API.v1.failure('Webhook_URL_not_set'); } try { diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index 52ccd0441e24..c541e5f7b2c3 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -7,7 +7,6 @@ import moment from 'moment'; import { closeBusinessHour } from '../../../../ee/app/livechat-enterprise/server/business-hour/Helper'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; -import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; export class BusinessHourManager { @@ -27,7 +26,6 @@ export class BusinessHourManager { async startManager(): Promise { await this.createCronJobsForWorkHours(); - businessHourLogger.debug('Cron jobs created, setting up callbacks'); this.setupCallbacks(); await this.cleanupDisabledDepartmentReferences(); await this.behavior.onStartBusinessHours(); diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e61bb1621765..e96ccb4c7b89 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -59,7 +59,6 @@ export const openBusinessHourDefault = async (): Promise => { await Users.makeAgentsWithinBusinessHourAvailable(); } await Users.updateLivechatStatusBasedOnBusinessHours(); - businessHourLogger.debug('Done opening default business hours'); }; export const createDefaultBusinessHourIfNotExists = async (): Promise => { diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index d899f2717376..5d2730dba9a1 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -8,7 +8,6 @@ import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { - businessHourLogger.debug('opening single business hour'); return openBusinessHourDefault(); } @@ -23,7 +22,6 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp } async onStartBusinessHours(): Promise { - businessHourLogger.debug('Starting Single Business Hours'); return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 30900481c4e2..0419f1d02a1d 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -3,7 +3,6 @@ import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; -import { callbackLogger } from '../lib/logger'; type IAfterSaveUserProps = { user: IUser; @@ -34,17 +33,12 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug({ - msg: 'Removing agent extension & making agent unavailable', - userId: user._id, - }); await Users.makeAgentUnavailableAndUnsetExtension(user._id); } }; const handleActivateUser = async (user: IUser) => { if (isAgent(user)) { - callbackLogger.debug('Adding agent', user._id); await Livechat.addAgent(user.username); } }; diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 5ebf924e7334..ad68fcf5ce5c 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -1,6 +1,7 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; -import { LivechatRooms } from '@rocket.chat/models'; +import { LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; +import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; @@ -26,6 +27,19 @@ callbacks.add( return message; } + // Return YYYY-MM from moment + const monthYear = moment().format('YYYY-MM'); + const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear); + if (!isVisitorActive) { + await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear); + } + + await LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear); + + if (room.responseBy) { + await LivechatRooms.setAgentLastMessageTs(room._id); + } + // check if room is yet awaiting for response from visitor if (!room.waitingResponse) { // case where agent sends second message or any subsequent message in a room before visitor responds to the first message diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index ec584ec001d6..e92e6b4d940b 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -3,7 +3,6 @@ import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; -import { callbackLogger } from '../lib/logger'; callbacks.add( 'afterSaveMessage', @@ -13,7 +12,6 @@ callbacks.add( return message; } - callbackLogger.debug(`Calculating Omnichannel metrics for room ${room._id}`); // skips this callback if the message was edited if (!message || isEditedMessage(message)) { return message; @@ -43,7 +41,6 @@ callbacks.add( const isResponseTotal = room.metrics?.response?.total; if (agentLastReply === room.ts) { - callbackLogger.debug('Calculating: first message from agent'); // first response const firstResponseDate = now; const firstResponseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000; @@ -66,7 +63,6 @@ callbacks.add( reactionTime, }; } else if (visitorLastQuery > agentLastReply) { - callbackLogger.debug('Calculating: visitor sent a message after agent'); // response, not first const responseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000; const avgResponseTime = diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 5f6e3469501e..28bed221afbf 100644 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ b/apps/meteor/app/livechat/server/lib/Analytics.js @@ -43,8 +43,6 @@ export const Analytics = { const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - logger.debug(`getAgentOverviewData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAgentOverviewData => Invalid dates'); return; @@ -79,8 +77,6 @@ export const Analytics = { const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); const isSameDay = from.diff(to, 'days') === 0; - logger.debug(`getAnalyticsChartData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAnalyticsChartData => Invalid dates'); return; @@ -133,8 +129,6 @@ export const Analytics = { const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - logger.debug(`getAnalyticsOverviewData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAnalyticsOverviewData => Invalid dates'); return; diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts index 0dd48a328fd1..f17015e52e79 100644 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -12,7 +12,6 @@ class DepartmentHelperClass { const department = await LivechatDepartment.findOneById(departmentId); if (!department) { - this.logger.debug(`Department not found: ${departmentId}`); throw new Error('error-department-not-found'); } @@ -20,10 +19,8 @@ class DepartmentHelperClass { const ret = await LivechatDepartment.removeById(_id); if (ret.acknowledged !== true) { - this.logger.error(`Department record not removed: ${_id}. Result from db: ${ret}`); throw new Error('error-failed-to-delete-department'); } - this.logger.debug(`Department record removed: ${_id}`); const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId>( department._id, @@ -47,8 +44,6 @@ class DepartmentHelperClass { } }); - this.logger.debug(`Post-department-removal actions completed: ${_id}. Notifying callbacks with department and agentsIds`); - setImmediate(() => { void callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); }); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 52138740e295..c560f3dd7aa7 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -285,7 +285,7 @@ export const Livechat = { Livechat.logger.debug(`Closing open chats for user ${userId}`); const user = await Users.findOneById(userId); - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}); + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); const promises = []; await openChats.forEach((room) => { @@ -298,7 +298,7 @@ export const Livechat = { async forwardOpenChats(userId) { Livechat.logger.debug(`Transferring open chats for user ${userId}`); for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneById(room.v._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); const user = await Users.findOneById(userId); const { _id, username, name } = user; const transferredBy = normalizeTransferredByData({ _id, username, name }, room); @@ -462,7 +462,7 @@ export const Livechat = { }, async getLivechatRoomGuestInfo(room) { - const visitor = await LivechatVisitors.findOneById(room.v._id); + const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); const agent = await Users.findOneById(room.servedBy && room.servedBy._id); const ua = new UAParser(); @@ -604,16 +604,15 @@ export const Livechat = { }, async removeGuest(_id) { - check(_id, String); - const guest = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); + const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest', }); } - await this.cleanGuestHistory(_id); - return LivechatVisitors.removeById(_id); + await this.cleanGuestHistory(guest); + return LivechatVisitors.disableById(_id); }, async setUserStatusLivechat(userId, status) { @@ -628,16 +627,13 @@ export const Livechat = { return user; }, - async cleanGuestHistory(_id) { - const guest = await LivechatVisitors.findOneById(_id); - if (!guest) { - throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { - method: 'livechat:cleanGuestHistory', - }); - } - + async cleanGuestHistory(guest) { const { token } = guest; - check(token, String); + + // This shouldn't be possible, but just in case + if (!token) { + throw new Error('error-invalid-guest'); + } const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const cursor = LivechatRooms.findByVisitorToken(token, extraQuery); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 1c60a257d319..c443bc7873c7 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -305,7 +305,7 @@ class LivechatClass { !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) ) { await LivechatVisitors.removeDepartmentById(guest._id); - const tmpGuest = await LivechatVisitors.findOneById(guest._id); + const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id); if (tmpGuest) { guest = tmpGuest; } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 597f38b71ec0..aed0061e808e 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -23,7 +23,6 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent const dbInquiry = await LivechatInquiry.findOneById(inquiry._id); if (!dbInquiry) { - logger.error(`Inquiry with id ${inquiry._id} not found`); throw new Error('inquiry-not-found'); } @@ -68,7 +67,6 @@ export const QueueManager: queueManager = { ); if (!(await checkServiceStatus({ guest, agent }))) { - logger.debug(`Cannot create room for visitor ${guest._id}. No online agents`); throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } @@ -96,8 +94,6 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } - logger.debug(`Generated inquiry for visitor ${guest._id} with id ${inquiry._id} [Not queued]`); - await LivechatRooms.updateRoomCount(); await queueInquiry(inquiry, agent); @@ -114,7 +110,6 @@ export const QueueManager: queueManager = { async unarchiveRoom(archivedRoom) { if (!archivedRoom) { - logger.error('No room to unarchive'); throw new Error('no-room-to-unarchive'); } @@ -145,17 +140,13 @@ export const QueueManager: queueManager = { await LivechatRooms.unarchiveOneById(rid); const room = await LivechatRooms.findOneById(rid); if (!room) { - logger.debug(`Room with id ${rid} not found`); throw new Error('room-not-found'); } const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } })); if (!inquiry) { - logger.error(`Inquiry for visitor ${guest._id} not found`); throw new Error('inquiry-not-found'); } - logger.debug(`Generated inquiry for visitor ${v._id} with id ${inquiry._id} [Not queued]`); - await queueInquiry(inquiry, defaultAgent); logger.debug(`Inquiry ${inquiry._id} queued`); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 0e975ca06763..f2fd7010eb12 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -74,7 +74,7 @@ export const RoutingManager: Routing = { }, async setMethodNameAndStartQueue(name) { - logger.debug(`Changing default routing method from ${this.methodName} to ${name}`); + logger.info(`Changing default routing method from ${this.methodName} to ${name}`); if (!this.methods[name]) { logger.warn(`Cannot change routing method to ${name}. Selected Routing method does not exists. Defaulting to Manual_Selection`); this.methodName = 'Manual_Selection'; @@ -87,7 +87,6 @@ export const RoutingManager: Routing = { // eslint-disable-next-line @typescript-eslint/naming-convention registerMethod(name, Method) { - logger.debug(`Registering new routing method with name ${name}`); this.methods[name] = new Method(); }, @@ -188,7 +187,6 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (servedBy) { - logger.debug(`Unassigning current agent for inquiry ${inquiry._id}`); await LivechatRooms.removeAgentByRoomId(rid); await this.removeAllRoomSubscriptions(room); await dispatchAgentDelegated(rid); @@ -254,7 +252,7 @@ export const RoutingManager: Routing = { await LivechatInquiry.takeInquiry(_id); const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); - logger.debug(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); + logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); @@ -262,7 +260,6 @@ export const RoutingManager: Routing = { }, async transferRoom(room, guest, transferData) { - logger.debug(`Transfering room ${room._id} by ${transferData.transferredBy._id}`); if (transferData.departmentId) { logger.debug(`Transfering room ${room._id} to department ${transferData.departmentId}`); return forwardRoomToDepartment(room, guest, transferData); @@ -278,7 +275,6 @@ export const RoutingManager: Routing = { }, async delegateAgent(agent, inquiry) { - logger.debug(`Delegating Inquiry ${inquiry._id}`); const defaultAgent = await callbacks.run('livechat.beforeDelegateAgent', agent, { department: inquiry?.department, }); diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts index 94fae239b74c..9cd5de75a0f3 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts @@ -18,9 +18,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAgentOverviewData'(options) { - methodDeprecationLogger.warn( - 'The method "livechat:getAgentOverviewData" is deprecated and will be removed after version v7.0.0. Use "livechat/analytics/agent-overview" instead.', - ); + methodDeprecationLogger.method('livechat:getAgentOverviewData', '7.0.0', ' Use "livechat/analytics/agent-overview" instead.'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts index 48313f1ce67c..76b7f276d671 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts @@ -3,6 +3,7 @@ import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { Livechat } from '../lib/Livechat'; @@ -18,6 +19,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAnalyticsOverviewData'(options) { + methodDeprecationLogger.method('livechat:getAnalyticsOverviewData', '7.0.0', ' Use "livechat/analytics/overview" instead.'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/methods/removeTrigger.ts b/apps/meteor/app/livechat/server/methods/removeTrigger.ts index c403dcd3edac..69f3a4a2d80c 100644 --- a/apps/meteor/app/livechat/server/methods/removeTrigger.ts +++ b/apps/meteor/app/livechat/server/methods/removeTrigger.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:removeTrigger'(triggerId) { + methodDeprecationLogger.method('livechat:removeTrigger', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index 2dc796fc6c94..3817b10bf42b 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -58,7 +58,7 @@ Meteor.methods({ }); } - const guest = await LivechatVisitors.findOneById(room.v?._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); const user = await Meteor.userAsync(); diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index ea220b24d149..2557fcdeb83d 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -10,33 +10,27 @@ import { callbackLogger } from './lib/logger'; callbacks.add( 'afterSaveMessage', async (message, room) => { - callbackLogger.debug('Attempting to send SMS message'); // skips this callback if the message was edited if (isEditedMessage(message)) { - callbackLogger.debug('Message was edited, skipping SMS send'); return message; } if (!settings.get('SMS_Enabled')) { - callbackLogger.debug('SMS is not enabled, skipping SMS send'); return message; } // only send the sms by SMS if it is a livechat room with SMS set to true if (!(isOmnichannelRoom(room) && room.sms && room.v && room.v.token)) { - callbackLogger.debug('Room is not a livechat room, skipping SMS send'); return message; } // if the message has a token, it was sent from the visitor, so ignore it if (message.token) { - callbackLogger.debug('Message was sent from the visitor, skipping SMS send'); return message; } // if the message has a type means it is a special message (like the closing comment), so skips if (message.t) { - callbackLogger.debug('Message is a special message, skipping SMS send'); return message; } @@ -52,8 +46,9 @@ callbacks.add( const { location } = message; extraData = Object.assign({}, extraData, { location }); } + const service = settings.get('SMS_Service'); - const SMSService = await OmnichannelIntegration.getSmsService(settings.get('SMS_Service')); + const SMSService = await OmnichannelIntegration.getSmsService(service); if (!SMSService) { callbackLogger.debug('SMS Service is not configured, skipping SMS send'); @@ -63,14 +58,12 @@ callbacks.add( const visitor = await LivechatVisitors.getVisitorByToken(room.v.token, { projection: { phone: 1 } }); if (!visitor?.phone || visitor.phone.length === 0) { - callbackLogger.debug('Visitor does not have a phone number, skipping SMS send'); return message; } try { - callbackLogger.debug(`Message will be sent to ${visitor.phone[0].phoneNumber} through service ${settings.get('SMS_Service')}`); await SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg, extraData); - callbackLogger.debug(`SMS message sent to ${visitor.phone[0].phoneNumber}`); + callbackLogger.debug(`SMS message sent to ${visitor.phone[0].phoneNumber} via ${service}`); } catch (e) { callbackLogger.error(e); } diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index f24f88975b22..f9fce509e39a 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -62,14 +62,12 @@ Meteor.startup(async () => { await createDefaultBusinessHourIfNotExists(); settings.watch('Livechat_enable_business_hours', async (value) => { - Livechat.logger.debug(`Changing business hour type to ${value}`); + Livechat.logger.info(`Changing business hour type to ${value}`); if (value) { await businessHourManager.startManager(); - Livechat.logger.debug(`Business hour manager started`); return; } await businessHourManager.stopManager(); - Livechat.logger.debug(`Business hour manager stopped`); }); settings.watch('Livechat_Routing_Method', (value) => { diff --git a/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts index 1d36400296b4..b1355c90cb93 100644 --- a/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts +++ b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts @@ -2,6 +2,7 @@ import { Team } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { callbacks } from '../../../lib/callbacks'; +import { settings } from '../../settings/server'; interface IExtraDataForNotification { userMentions: any[]; @@ -9,7 +10,7 @@ interface IExtraDataForNotification { message: IMessage; } -callbacks.add('beforeGetMentions', async (mentionIds: string[], extra?: IExtraDataForNotification) => { +const beforeGetMentions = async (mentionIds: string[], extra?: IExtraDataForNotification) => { const { otherMentions } = extra ?? {}; const teamIds = otherMentions?.filter(({ type }) => type === 'team').map(({ _id }) => _id); @@ -22,4 +23,13 @@ callbacks.add('beforeGetMentions', async (mentionIds: string[], extra?: IExtraDa mentionIds.push(...new Set(members.map(({ userId }) => userId).filter((userId) => !mentionIds.includes(userId)))); return mentionIds; +}; + +settings.watch('Troubleshoot_Disable_Teams_Mention', (value) => { + if (value) { + callbacks.remove('beforeGetMentions', 'before-get-mentions-get-teams'); + return; + } + + callbacks.add('beforeGetMentions', beforeGetMentions, callbacks.priority.MEDIUM, 'before-get-mentions-get-teams'); }); diff --git a/apps/meteor/app/mentions/server/server.ts b/apps/meteor/app/mentions/server/server.ts index a5b0f89526a2..13765e99d856 100644 --- a/apps/meteor/app/mentions/server/server.ts +++ b/apps/meteor/app/mentions/server/server.ts @@ -25,6 +25,10 @@ export class MentionQueries { type: 'user' as const, })); + if (settings.get('Troubleshoot_Disable_Teams_Mention')) { + return taggedUsers; + } + const taggedTeams = teams.map((team) => ({ ...team, type: 'team' as const, diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index a36883abb0a5..906f0c98c181 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -1,6 +1,6 @@ import { Message } from '@rocket.chat/core-services'; -import { isQuoteAttachment } from '@rocket.chat/core-typings'; -import type { IMessage, IUser, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; +import { isQuoteAttachment, isRegisterUser } from '@rocket.chat/core-typings'; +import type { IMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { Messages, Rooms, Subscriptions, Users, ReadReceipts } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; @@ -82,15 +82,13 @@ Meteor.methods({ throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); } - const me = await Users.findOneById>>(userId, { - projection: { username: 1, name: 1 }, - }); + const me = await Users.findOneById(userId); if (!me) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'pinMessage' }); } // If we keep history of edits, insert a new message to store history information - if (settings.get('Message_KeepHistory') && me.username) { + if (settings.get('Message_KeepHistory') && isRegisterUser(me)) { await Messages.cloneAndSaveAsHistoryById(message._id, me); } @@ -110,6 +108,8 @@ Meteor.methods({ username: me.username, }; + originalMessage = await Message.beforeSave({ message: originalMessage, room, user: me }); + originalMessage = await callbacks.run('beforeSaveMessage', originalMessage); await Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); @@ -186,15 +186,13 @@ Meteor.methods({ throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'unpinMessage' }); } - const me = await Users.findOneById>>(userId, { - projection: { username: 1, name: 1 }, - }); + const me = await Users.findOneById(userId); if (!me) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unpinMessage' }); } // If we keep history of edits, insert a new message to store history information - if (settings.get('Message_KeepHistory') && me.username) { + if (settings.get('Message_KeepHistory') && isRegisterUser(me)) { await Messages.cloneAndSaveAsHistoryById(originalMessage._id, me); } @@ -203,7 +201,6 @@ Meteor.methods({ _id: userId, username: me.username, }; - originalMessage = await callbacks.run('beforeSaveMessage', originalMessage); const room = await Rooms.findOneById(originalMessage.rid, { projection: { ...roomAccessAttributes, lastMessage: 1 } }); if (!room) { @@ -214,6 +211,10 @@ Meteor.methods({ throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'unpinMessage' }); } + originalMessage = await Message.beforeSave({ message: originalMessage, room, user: me }); + + originalMessage = await callbacks.run('beforeSaveMessage', originalMessage); + if (isTheLastMessage(room, message)) { await Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); } 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 06c3014a8a56..f62ab71f2302 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -480,7 +480,6 @@ export class SAML { continue; } - const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); const privRoom = await Rooms.findOneByNameAndType(roomName, 'p', {}); if (privRoom && includePrivateChannelsInUpdate === true) { @@ -488,6 +487,7 @@ export class SAML { continue; } + const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); if (room) { await addUserToRoom(room._id, user); continue; @@ -496,7 +496,7 @@ export class SAML { if (!room && !privRoom) { // If the user doesn't have an username yet, we can't create new rooms for them if (user.username) { - await createRoom('c', roomName, user.username); + await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts index e80e456c679b..d2d0f85d19ce 100644 --- a/apps/meteor/app/oembed/server/providers.ts +++ b/apps/meteor/app/oembed/server/providers.ts @@ -1,9 +1,5 @@ -import QueryString from 'querystring'; -import URL from 'url'; - -import type { OEmbedMeta, OEmbedUrlContent, ParsedUrl, OEmbedProvider } from '@rocket.chat/core-typings'; +import type { OEmbedMeta, OEmbedUrlContent, OEmbedProvider } from '@rocket.chat/core-typings'; import { camelCase } from 'change-case'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; @@ -16,10 +12,10 @@ class Providers { } static getConsumerUrl(provider: OEmbedProvider, url: string): string { - const urlObj = new URL.URL(provider.endPoint); + const urlObj = new URL(provider.endPoint); urlObj.searchParams.set('url', url); - return URL.format(urlObj); + return urlObj.toString(); } registerProvider(provider: OEmbedProvider): number { @@ -95,25 +91,20 @@ providers.registerProvider({ callbacks.add( 'oembed:beforeGetUrlContent', (data) => { - if (data.parsedUrl != null) { - const url = URL.format(data.parsedUrl); - const provider = providers.getProviderForUrl(url); - if (provider != null) { - const consumerUrl = Providers.getConsumerUrl(provider, url); - - const parsedConsumerUrl = URL.parse(consumerUrl, true); - _.extend(data.parsedUrl, parsedConsumerUrl); - - data.urlObj.port = parsedConsumerUrl.port; - data.urlObj.hostname = parsedConsumerUrl.hostname; - data.urlObj.pathname = parsedConsumerUrl.pathname; - data.urlObj.query = parsedConsumerUrl.query; - - delete data.urlObj.search; - delete data.urlObj.host; - } + if (!data.urlObj) { + return data; } - return data; + + const url = data.urlObj.toString(); + const provider = providers.getProviderForUrl(url); + + if (!provider) { + return data; + } + + const consumerUrl = Providers.getConsumerUrl(provider, url); + + return { ...data, urlObj: new URL(consumerUrl) }; }, callbacks.priority.MEDIUM, 'oembed-providers-before', @@ -123,13 +114,11 @@ const cleanupOembed = (data: { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; }): { url: string; meta: Omit; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; } => { if (!data?.meta) { @@ -148,24 +137,17 @@ const cleanupOembed = (data: { callbacks.add( 'oembed:afterParseContent', (data) => { - if (!data?.url || !data.content?.body || !data.parsedUrl?.query) { + if (!data?.url || !data.content?.body) { return cleanupOembed(data); } - const queryString = typeof data.parsedUrl.query === 'string' ? QueryString.parse(data.parsedUrl.query) : data.parsedUrl.query; - - if (!queryString.url) { - return cleanupOembed(data); - } + const provider = providers.getProviderForUrl(data.url); - const { url: originalUrl } = data; - const provider = providers.getProviderForUrl(originalUrl); if (!provider) { return cleanupOembed(data); } - const { url } = queryString; - data.meta.oembedUrl = url; + data.meta.oembedUrl = data.url; try { const metas = JSON.parse(data.content.body); diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts index 256722cdd3d4..79de0402043f 100644 --- a/apps/meteor/app/oembed/server/server.ts +++ b/apps/meteor/app/oembed/server/server.ts @@ -1,6 +1,3 @@ -import querystring from 'querystring'; -import URL from 'url'; - import type { OEmbedUrlContentResult, OEmbedUrlWithMetadata, IMessage, MessageAttachment, OEmbedMeta } from '@rocket.chat/core-typings'; import { isOEmbedUrlContentResult, isOEmbedUrlWithMetadata } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -11,7 +8,6 @@ import he from 'he'; import iconv from 'iconv-lite'; import ipRangeCheck from 'ip-range-check'; import jschardet from 'jschardet'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; @@ -62,14 +58,7 @@ const toUtf8 = function (contentType: string, body: Buffer): string { return iconv.decode(body, getCharset(contentType, body)); }; -const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery, redirectCount = 5): Promise { - let urlObj: URL.UrlWithStringQuery; - if (typeof urlObjStr === 'string') { - urlObj = URL.parse(urlObjStr); - } else { - urlObj = urlObjStr; - } - +const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise => { const portsProtocol = new Map( Object.entries({ 80: 'http:', @@ -78,34 +67,28 @@ const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery }), ); - const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; - if (parsedUrl.hostname && (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts))) { + if (urlObj.hostname && (ignoredHosts.includes(urlObj.hostname) || ipRangeCheck(urlObj.hostname, ignoredHosts))) { throw new Error('invalid host'); } const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; - if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { + // checks if the URL port is in the safe ports list + if (safePorts.length > 0 && urlObj.port && !safePorts.includes(urlObj.port)) { throw new Error('invalid/unsafe port'); } - if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol.get(port) === parsedUrl.protocol)) { + // if port is not detected, use protocol to verify instead + if (safePorts.length > 0 && !urlObj.port && !safePorts.some((port) => portsProtocol.get(port) === urlObj.protocol)) { throw new Error('invalid/unsafe port'); } const data = await callbacks.run('oembed:beforeGetUrlContent', { urlObj, - parsedUrl, }); - /* This prop is neither passed or returned by the callback, so I'll just comment it for now - if (data.attachments != null) { - return data; - } */ - - const url = URL.format(data.urlObj); - + const url = data.urlObj.toString(); const sizeLimit = 250000; log.debug(`Fetching ${url} following redirects ${redirectCount} times`); @@ -137,10 +120,10 @@ const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery log.debug('Obtained response from server with length of', totalSize); const buffer = Buffer.concat(chunks); + return { headers: Object.fromEntries(response.headers), body: toUtf8(response.headers.get('content-type') || 'text/plain', buffer), - parsedUrl, statusCode: response.status, }; }; @@ -150,19 +133,13 @@ const getUrlMeta = async function ( withFragment?: boolean, ): Promise { log.debug('Obtaining metadata for URL', url); - const urlObj = URL.parse(url); - if (withFragment != null) { - const queryStringObj = querystring.parse(urlObj.query || ''); - queryStringObj._escaped_fragment_ = ''; - urlObj.query = querystring.stringify(queryStringObj); - let path = urlObj.pathname; - if (urlObj.query != null) { - path += `?${urlObj.query}`; - urlObj.search = `?${urlObj.query}`; - } - urlObj.path = path; + const urlObj = new URL(url); + + if (withFragment) { + urlObj.searchParams.set('_escaped_fragment_', ''); } - log.debug('Fetching url content', urlObj.path); + + log.debug('Fetching url content', urlObj.toString()); let content: OEmbedUrlContentResult | undefined; try { content = await getUrlContent(urlObj, 5); @@ -174,7 +151,7 @@ const getUrlMeta = async function ( return; } - if (content.attachments != null) { + if (content.attachments) { return content; } @@ -221,7 +198,6 @@ const getUrlMeta = async function ( url, meta: metas, headers, - parsedUrl: content.parsedUrl, content, }); }; @@ -233,38 +209,25 @@ const getUrlMetaWithCache = async function ( log.debug('Getting oembed metadata for', url); const cache = await OEmbedCache.findOneById(url); - if (cache != null) { + if (cache) { log.debug('Found oembed metadata in cache for', url); return cache.data; } + const data = await getUrlMeta(url, withFragment); - if (data != null) { - try { - log.debug('Saving oembed metadata in cache for', url); - await OEmbedCache.createWithIdAndData(url, data); - } catch (_error) { - log.error({ msg: 'OEmbed duplicated record', url }); - } - return data; - } -}; -const hasOnlyContentLength = (obj: any): obj is { contentLength: string } => 'contentLength' in obj && Object.keys(obj).length === 1; -const hasOnlyContentType = (obj: any): obj is { contentType: string } => 'contentType' in obj && Object.keys(obj).length === 1; -const hasContentLengthAndContentType = (obj: any): obj is { contentLength: string; contentType: string } => - 'contentLength' in obj && 'contentType' in obj && Object.keys(obj).length === 2; - -const getRelevantHeaders = function (headersObj: { - [key: string]: string; -}): { contentLength: string } | { contentType: string } | { contentLength: string; contentType: string } | void { - const headers = { - ...(headersObj.contentLength && { contentLength: headersObj.contentLength }), - ...(headersObj.contentType && { contentType: headersObj.contentType }), - }; + if (!data) { + return; + } - if (hasOnlyContentLength(headers) || hasOnlyContentType(headers) || hasContentLengthAndContentType(headers)) { - return headers; + try { + log.debug('Saving oembed metadata in cache for', url); + await OEmbedCache.createWithIdAndData(url, data); + } catch (_error) { + log.error({ msg: 'OEmbed duplicated record', url }); } + + return data; }; const getRelevantMetaTags = function (metaObj: OEmbedMeta): Record | void { @@ -286,57 +249,71 @@ const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => const rocketUrlParser = async function (message: IMessage): Promise { log.debug('Parsing message URLs'); - if (Array.isArray(message.urls)) { - log.debug('URLs found', message.urls.length); - - if ( - (message.attachments && message.attachments.length > 0) || - message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS - ) { - log.debug('All URL ignored'); - return message; + + if (!Array.isArray(message.urls)) { + return message; + } + + log.debug('URLs found', message.urls.length); + + if ( + (message.attachments && message.attachments.length > 0) || + message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS + ) { + log.debug('All URL ignored'); + return message; + } + + const attachments: MessageAttachment[] = []; + + let changed = false; + for await (const item of message.urls) { + if (item.ignoreParse === true) { + log.debug('URL ignored', item.url); + continue; } - const attachments: MessageAttachment[] = []; + if (!isURL(item.url)) { + continue; + } - let changed = false; - for await (const item of message.urls) { - if (item.ignoreParse === true) { - log.debug('URL ignored', item.url); - continue; - } - if (!isURL(item.url)) { - continue; - } - const data = await getUrlMetaWithCache(item.url); - if (data != null) { - if (isOEmbedUrlContentResult(data) && data.attachments) { - attachments.push(...data.attachments); - break; - } - if (isOEmbedUrlWithMetadata(data) && data.meta != null) { - item.meta = getRelevantMetaTags(data.meta) || {}; - if (item.meta?.oembedHtml) { - item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; - } - } - if (data.headers != null) { - const headers = getRelevantHeaders(data.headers); - if (headers) { - item.headers = headers; - } - } - item.parsedUrl = data.parsedUrl; - changed = true; + const data = await getUrlMetaWithCache(item.url); + + if (!data) { + continue; + } + + if (isOEmbedUrlContentResult(data) && data.attachments) { + attachments.push(...data.attachments); + break; + } + + if (isOEmbedUrlWithMetadata(data) && data.meta) { + item.meta = getRelevantMetaTags(data.meta) || {}; + if (item.meta?.oembedHtml) { + item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; } } - if (attachments.length > 0) { - await Messages.setMessageAttachments(message._id, attachments); + + if (data.headers?.contentLength) { + item.headers = { ...item.headers, contentLength: data.headers.contentLength }; } - if (changed === true) { - await Messages.setUrlsById(message._id, message.urls); + + if (data.headers?.contentType) { + item.headers = { ...item.headers, contentType: data.headers.contentType }; } + + changed = true; } + + if (attachments.length) { + await Messages.setMessageAttachments(message._id, attachments); + } + + if (changed === true) { + await Messages.setUrlsById(message._id, message.urls); + } + return message; }; diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index a3c70f012fa1..104d50c56926 100644 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ b/apps/meteor/app/slashcommands-create/server/server.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; @@ -50,7 +50,11 @@ slashCommands.add({ } if (getParams(params).indexOf('private') > -1) { - await createPrivateGroupMethod(userId, channelStr, []); + const user = await Users.findOneById(userId); + if (!user) { + return; + } + await createPrivateGroupMethod(user, channelStr, []); return; } diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 9917775aca06..5376bd6ae64b 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -37,6 +37,9 @@ function inviteAll(type: T): SlashCommand['callback'] { } const user = await Users.findOneById(userId); + if (!user) { + return; + } const lng = user?.language || settings.get('Language') || 'en'; const baseChannel = type === 'to' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel); @@ -69,7 +72,7 @@ function inviteAll(type: T): SlashCommand['callback'] { const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { - baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(userId, channel, users); + baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(user, channel, users); void api.broadcast('notify.ephemeralMessage', userId, message.rid, { msg: i18n.t('Channel_created', { postProcess: 'sprintf', diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index dfe27d8d5dc4..33d0278f81a3 100644 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ b/apps/meteor/app/slashcommands-join/server/server.ts @@ -1,10 +1,9 @@ -import { api } from '@rocket.chat/core-services'; +import { api, Room } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; -import { joinRoomMethod } from '../../lib/server/methods/joinRoom'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -16,13 +15,13 @@ slashCommands.add({ return; } - channel = channel.replace('#', ''); - const room = await Rooms.findOneByNameAndType(channel, 'c'); - if (!userId) { return; } + channel = channel.replace('#', ''); + + const room = await Rooms.findOneByNameAndType(channel, 'c'); if (!room) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { msg: i18n.t('Channel_doesnt_exist', { @@ -44,7 +43,7 @@ slashCommands.add({ }); } - await joinRoomMethod(userId, room._id); + await Room.join({ room, user: { _id: userId } }); }, options: { description: 'Join_the_given_channel', diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8cfe45b42232..64543deb88a1 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -24,10 +24,12 @@ import { LivechatCustomField, Subscriptions, Users, + LivechatRooms, } from '@rocket.chat/models'; import { MongoInternals } from 'meteor/mongo'; +import moment from 'moment'; -import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; +import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; @@ -41,8 +43,6 @@ import { getAppsStatistics } from './getAppsStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; -const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; - const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => { const result = await Users.getUserLanguages(); @@ -70,17 +70,29 @@ export const statistics = { const statistics = {} as IStats; const statsPms = []; + const fetchWizardSettingValue = async (settingName: string): Promise => { + return ((await Settings.findOne(settingName))?.value as T | undefined) ?? undefined; + }; + // Setup Wizard - statistics.wizard = {}; - await Promise.all( - wizardFields.map(async (field) => { - const record = await Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }), - ); + const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([ + fetchWizardSettingValue('Organization_Type'), + fetchWizardSettingValue('Industry'), + fetchWizardSettingValue('Size'), + fetchWizardSettingValue('Country'), + fetchWizardSettingValue('Language'), + fetchWizardSettingValue('Server_Type'), + fetchWizardSettingValue('Register_Server'), + ]); + statistics.wizard = { + organizationType, + industry, + size, + country, + language, + serverType, + registerServer, + }; // Version const uniqueID = await Settings.findOne('uniqueID'); @@ -89,6 +101,9 @@ export const statistics = { statistics.installedAt = uniqueID.createdAt.toISOString(); } + statistics.deploymentFingerprintHash = settings.get('Deployment_FingerPrint_Hash'); + statistics.deploymentFingerprintVerified = settings.get('Deployment_FingerPrint_Verified'); + if (Info) { statistics.version = Info.version; statistics.tag = Info.tag; @@ -259,6 +274,36 @@ export const statistics = { }), ); + const defaultValue = { contactsCount: 0, conversationsCount: 0, sources: [] }; + const billablePeriod = moment.utc().format('YYYY-MM'); + statsPms.push( + LivechatRooms.getMACStatisticsForPeriod(billablePeriod).then(([result]) => { + statistics.omnichannelContactsBySource = result || defaultValue; + }), + ); + + const monthAgo = moment.utc().subtract(30, 'days').toDate(); + const today = moment.utc().toDate(); + statsPms.push( + LivechatRooms.getMACStatisticsBetweenDates(monthAgo, today).then(([result]) => { + statistics.uniqueContactsOfLastMonth = result || defaultValue; + }), + ); + + const weekAgo = moment.utc().subtract(7, 'days').toDate(); + statsPms.push( + LivechatRooms.getMACStatisticsBetweenDates(weekAgo, today).then(([result]) => { + statistics.uniqueContactsOfLastWeek = result || defaultValue; + }), + ); + + const yesterday = moment.utc().subtract(1, 'days').toDate(); + statsPms.push( + LivechatRooms.getMACStatisticsBetweenDates(yesterday, today).then(([result]) => { + statistics.uniqueContactsOfYesterday = result || defaultValue; + }), + ); + // Message statistics statistics.totalChannelMessages = (await Rooms.findByType('c', { projection: { msgs: 1 } }).toArray()).reduce( function _countChannelMessages(num: number, room: IRoom) { @@ -507,6 +552,15 @@ export const statistics = { statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); + const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; + + // one bit for each of the following: + const pushEnabled = settings.get('Push_enable') ? 1 : 0; + const pushGatewayEnabled = settings.get('Push_enable_gateway') ? 2 : 0; + const pushGatewayChanged = settings.get('Push_gateway') !== defaultGateway ? 4 : 0; + + statistics.push = pushEnabled | pushGatewayEnabled | pushGatewayChanged; + const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue; statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; diff --git a/apps/meteor/app/theme/client/imports/general/base.css b/apps/meteor/app/theme/client/imports/general/base.css index 311327fb6f73..d1f4b8d11fb6 100644 --- a/apps/meteor/app/theme/client/imports/general/base.css +++ b/apps/meteor/app/theme/client/imports/general/base.css @@ -52,12 +52,6 @@ body { a { cursor: pointer; - text-decoration: none; - - &:hover, - &:active { - text-decoration: none; - } } button { diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 78112bcee343..1e00a0e47433 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -32,7 +32,7 @@ const callback: NextHandleFunction = (req, res, next) => { return; } - const injection = headInjections.get(pathname.replace(/^\//, '')) as Injection | undefined; + const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]) as Injection | undefined; if (!injection || typeof injection === 'string') { next(); @@ -76,27 +76,37 @@ export const injectIntoHead = (key: string, value: Injection): void => { }; export const addScript = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addScript - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.js`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.js`, { + + injectIntoHead(key, { type: 'JS', - tag: ``, + tag: ``, content, }); }; export const addStyle = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addStyle - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.css`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.css`, { + + injectIntoHead(key, { type: 'CSS', - tag: ``, + tag: ``, content, }); }; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 4609797e6bd2..a926f8540d27 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -48,13 +48,15 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) text: string, { selection, + skipFocus, }: { selection?: | { readonly start?: number; readonly end?: number } | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); + skipFocus?: boolean; } = {}, ): void => { - focus(); + !skipFocus && focus(); const { selectionStart, selectionEnd } = input; const textAreaTxt = input.value; @@ -66,7 +68,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) if (selection) { if (!document.execCommand?.('insertText', false, text)) { input.value = textAreaTxt.substring(0, selectionStart) + text + textAreaTxt.substring(selectionStart); - focus(); + !skipFocus && focus(); } input.setSelectionRange(selection.start ?? 0, selection.end ?? text.length); } @@ -78,7 +80,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) triggerEvent(input, 'input'); triggerEvent(input, 'change'); - focus(); + !skipFocus && focus(); }; const insertText = (text: string): void => { @@ -260,7 +262,9 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const insertNewLine = (): void => insertText('\n'); - setText(Meteor._localStorage.getItem(storageID) ?? ''); + setText(Meteor._localStorage.getItem(storageID) ?? '', { + skipFocus: true, + }); // Gets the text that is connected to the cursor and replaces it with the given text const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => { diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 5618442ee6da..5807673188e5 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -29,7 +29,7 @@ Meteor.startup(async () => { id: 'reply-directly', icon: 'reply-directly', label: 'Reply_in_direct_message', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], role: 'link', type: 'communication', action(_, props) { @@ -122,7 +122,7 @@ Meteor.startup(async () => { icon: 'permalink', label: 'Copy_link', // classes: 'clipboard', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], type: 'duplication', async action(_, props) { try { @@ -208,7 +208,7 @@ Meteor.startup(async () => { id: 'delete-message', icon: 'trash', label: 'Delete', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', async action(this: unknown, _, { message = messageArgs(this).msg, chat }) { @@ -236,7 +236,7 @@ Meteor.startup(async () => { id: 'report-message', icon: 'report', label: 'Report', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', action(this: unknown, _, { message = messageArgs(this).msg }) { @@ -264,7 +264,7 @@ Meteor.startup(async () => { id: 'reaction-list', icon: 'emoji', label: 'Reactions', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'interaction', action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { imperativeModal.open({ diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index 91ef0989bd19..040b6dfa9dc2 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -1,13 +1,19 @@ import { settings } from '../../settings/client'; import { getURLWithoutSettings } from '../lib/getURL'; +import { Info } from '../rocketchat.info'; export const getURL = function ( path: string, // eslint-disable-next-line @typescript-eslint/naming-convention params: Record = {}, cloudDeepLinkUrl?: string, + cacheKey?: boolean, ): string { const cdnPrefix = settings.get('CDN_PREFIX') || ''; const siteUrl = settings.get('Site_Url') || ''; + if (cacheKey) { + path += `${path.includes('?') ? '&' : '?'}cacheKey=${Info.version}`; + } + return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl); }; diff --git a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts index a388548c18a8..adb4c2ab1ae9 100644 --- a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts +++ b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts @@ -1,4 +1,4 @@ -import type { ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings'; /** * @type {(userPref: Pick) => { @@ -7,7 +7,7 @@ import type { ISubscription, IUser } from '@rocket.chat/core-typings'; * emailPrefOrigin: 'user'; * }} */ -export const getDefaultSubscriptionPref = (userPref: IUser) => { +export const getDefaultSubscriptionPref = (userPref: AtLeast) => { const subscription: Partial = {}; const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {}; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index a5d2fcb9bc57..b9e235456291 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.4.0-develop" + "version": "6.5.0-develop" } diff --git a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts index ac0c0e443453..d17191a09be7 100644 --- a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts +++ b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts @@ -50,18 +50,16 @@ export const getNewUpdates = async () => { infoUrl: String, }), ], - alerts: [ - Match.Optional([ - Match.ObjectIncluding({ - id: String, - title: String, - text: String, - textArguments: [Match.Any], - modifiers: [String] as [StringConstructor], - infoUrl: String, - }), - ]), - ], + alerts: Match.Optional([ + Match.ObjectIncluding({ + id: String, + title: String, + text: String, + textArguments: [Match.Any], + modifiers: [String] as [StringConstructor], + infoUrl: String, + }), + ]), }), ); diff --git a/apps/meteor/client/components/ActionManagerBusyState.tsx b/apps/meteor/client/components/ActionManagerBusyState.tsx index dcf82342917c..0374254a7de9 100644 --- a/apps/meteor/client/components/ActionManagerBusyState.tsx +++ b/apps/meteor/client/components/ActionManagerBusyState.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState } from 'react'; @@ -23,10 +24,23 @@ const ActionManagerBusyState = () => { if (busy) { return ( - - - {t('Loading')} - + + {t('Loading')} ); } diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index 50d53da351bc..38aadf0a840b 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -17,7 +17,7 @@ type AutoCompleteDepartmentMultipleProps = { }; const AutoCompleteDepartmentMultiple = ({ - value, + value = [], onlyMyDepartments = false, showArchived = false, enabled = false, @@ -37,6 +37,11 @@ const AutoCompleteDepartmentMultiple = ({ const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); + const departmentOptions = useMemo(() => { + const pending = value.filter(({ value }) => !departmentsItems.find((dep) => dep.value === value)) || []; + return [...departmentsItems, ...pending]; + }, [departmentsItems, value]); + return ( ) => } + wrapperFunction={(props) => } > {t('Discussion_title')} - + + {t('Discussion_description')} - {t('Discussion_description')} - - - {t('Discussion_target_channel')} - + + {t('Discussion_target_channel')} + + {defaultParentRoom && ( } /> )} - {!defaultParentRoom && ( ( + rules={{ required: t('error-the-field-is-required', { field: t('Discussion_target_channel') }) }} + render={({ field: { name, onBlur, onChange, value } }) => ( )} /> )} - - {errors.parentRoom && {errors.parentRoom.message}} + + {errors.parentRoom && ( + + {errors.parentRoom.message} + + )} - - - {t('Encrypted')} - - ( - + + {t('Encrypted')} + + } /> - )} - /> + + - {t('Discussion_name')} - - } + + {t('Discussion_name')} + + + ( + } + /> + )} /> - - {errors.name && {errors.name.message}} + + {errors.name && ( + + {errors.name.message} + + )} - {t('Invite_Users')} - + {t('Invite_Users')} + ( - + render={({ field: { name, onChange, value, onBlur } }) => ( + )} /> - + - {t('Discussion_first_message_title')} - - - - {encrypted && {t('Discussion_first_message_disabled_due_to_e2e')}} + {t('Discussion_first_message_title')} + + ( + + )} + /> + + {encrypted && {t('Discussion_first_message_disabled_due_to_e2e')}} - diff --git a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx index 0a2717a65552..6036f14049a4 100644 --- a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx +++ b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx @@ -1,38 +1,42 @@ import { Skeleton, TextInput, Callout } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import { AsyncStatePhase } from '../../hooks/useAsyncState'; -import { useEndpointData } from '../../hooks/useEndpointData'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; const DefaultParentRoomField = ({ defaultParentRoom }: { defaultParentRoom: string }): ReactElement => { const t = useTranslation(); - const { value, phase } = useEndpointData('/v1/rooms.info', { - params: useMemo( - () => ({ - roomId: defaultParentRoom, - }), - [defaultParentRoom], - ), + + const query = useMemo( + () => ({ + roomId: defaultParentRoom, + }), + [defaultParentRoom], + ); + + const roomsInfoEndpoint = useEndpoint('GET', '/v1/rooms.info'); + + const { data, isLoading, isError } = useQuery(['defaultParentRoomInfo', query], async () => roomsInfoEndpoint(query), { + refetchOnWindowFocus: false, }); - if (phase === AsyncStatePhase.LOADING) { + if (isLoading) { return ; } - if (!value || !value.room) { + if (!data?.room || isError) { return {t('Error')}; } return ( diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx new file mode 100644 index 000000000000..db4c33654a92 --- /dev/null +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -0,0 +1,44 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import GenericModal from './GenericModal'; + +type FingerprintChangeModalProps = { + onConfirm: () => void; + onCancel: () => void; + onClose: () => void; +}; + +const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { + const t = useTranslation(); + return ( + + + + + ); +}; + +export default FingerprintChangeModal; diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx new file mode 100644 index 000000000000..77718de0f441 --- /dev/null +++ b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx @@ -0,0 +1,47 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import GenericModal from './GenericModal'; + +type FingerprintChangeModalConfirmationProps = { + onConfirm: () => void; + onCancel: () => void; + newWorkspace: boolean; +}; + +const FingerprintChangeModalConfirmation = ({ + onConfirm, + onCancel, + newWorkspace, +}: FingerprintChangeModalConfirmationProps): ReactElement => { + const t = useTranslation(); + return ( + + + + + ); +}; + +export default FingerprintChangeModalConfirmation; diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx index e02d6dc4e746..f660b4b85f35 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx @@ -1,5 +1,4 @@ -import type { IconButton } from '@rocket.chat/fuselage'; -import { MenuItem, MenuSection, MenuV2 } from '@rocket.chat/fuselage'; +import { IconButton, MenuItem, MenuSection, MenuV2 } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactNode } from 'react'; import React from 'react'; @@ -43,6 +42,12 @@ const GenericMenu = ({ title, icon = 'menu', onAction, ...props }: GenericMenuPr const handleItems = (items: GenericMenuItemProps[]) => hasIcon ? items.map((item) => ({ ...item, gap: !item.icon && !item.status })) : items; + const isMenuEmpty = !(sections && sections.length > 0) && !(items && items.length > 0); + + if (isMenuEmpty) { + return ; + } + return ( <> {sections && ( diff --git a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx index ec9f852c8a8b..513b31dd81fa 100644 --- a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx +++ b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx @@ -42,7 +42,7 @@ const GenericUpsellModal = ({ {icon && } - {tagline ?? t('Enterprise_capability')} + {tagline ?? t('Premium_capability')} {title} diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index 39564ca7f89f..88f5f1a5c6e7 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -1,4 +1,4 @@ -import { Field, TextInput, Chip, Button } from '@rocket.chat/fuselage'; +import { TextInput, Chip, Button, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; @@ -71,12 +71,12 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) return ( <> - + {t('Tags')} - + {EETagsComponent && tagsResult?.tags && tagsResult?.tags.length ? ( - + { @@ -85,10 +85,10 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) department={department} viewAll={!department} /> - + ) : ( <> - + {t('Add')} - + )} {customTags.length > 0 && ( - + {customTags?.map((tag, i) => ( removeTag(tag)} mie={8}> {tag} ))} - + )} ); diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index 3b39ea79d42f..fd3c0a29effe 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -20,7 +20,6 @@ type DepartmentListItem = { _id: string; label: string; value: string; - _updatedAt: Date; }; export const useDepartmentsList = ( @@ -66,7 +65,6 @@ export const useDepartmentsList = ( _id, label: department.archived ? `${name} [${t('Archived')}]` : name, value: _id, - _updatedAt: new Date(_updatedAt || ''), }; }); @@ -75,7 +73,6 @@ export const useDepartmentsList = ( _id: '', label: t('All'), value: 'all', - _updatedAt: new Date(), }); options.haveNone && @@ -83,7 +80,6 @@ export const useDepartmentsList = ( _id: '', label: t('None'), value: '', - _updatedAt: new Date(), }); return { diff --git a/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts b/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts index 4bd85be40342..ce5704b66482 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts @@ -1,6 +1,8 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; +import { useOmnichannel } from '../../../hooks/omnichannel/useOmnichannel'; + type Props = { department?: string; text?: string; @@ -9,13 +11,19 @@ type Props = { export const useLivechatTags = (options: Props) => { const getTags = useEndpoint('GET', '/v1/livechat/tags'); + const { isEnterprise } = useOmnichannel(); const { department, text, viewAll } = options; - return useQuery(['/v1/livechat/tags', text, department], () => - getTags({ - text: text || '', - ...(department && { department }), - viewAll: viewAll ? 'true' : 'false', - }), + return useQuery( + ['/v1/livechat/tags', text, department], + () => + getTags({ + text: text || '', + ...(department && { department }), + viewAll: viewAll ? 'true' : 'false', + }), + { + enabled: isEnterprise, + }, ); }; diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index 17cbc094160b..67d650186680 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -1,5 +1,18 @@ import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import { Field, FieldGroup, Button, TextInput, Modal, Box, CheckBox, Divider, EmailInput } from '@rocket.chat/fuselage'; +import { + Field, + FieldGroup, + Button, + TextInput, + Modal, + Box, + CheckBox, + Divider, + EmailInput, + FieldLabel, + FieldRow, + FieldError, +} from '@rocket.chat/fuselage'; import { usePermission, useSetting, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; @@ -134,8 +147,8 @@ const CloseChatModal = ({ {t('Close_room_description')} - {t('Comment')} - + {t('Comment')} + - - {errors.comment?.message} + + {errors.comment?.message} - {errors.tags?.message} + {errors.tags?.message} {canSendTranscript && ( <> - {t('Chat_transcript')} + {t('Chat_transcript')} {canSendTranscriptPDF && ( - + - + {t('Omnichannel_transcript_pdf')} - - + + )} {canSendTranscriptEmail && ( <> - + - + {t('Omnichannel_transcript_email')} - - + + {transcriptEmail && ( <> - {t('Contact_email')} - + {t('Contact_email')} + - + - {t('Subject')} - + {t('Subject')} + - - {errors.subject?.message} + + {errors.subject?.message} )} )} - + {canSendTranscriptPDF && canSendTranscriptEmail ? t('These_options_affect_this_conversation_only_To_set_default_selections_go_to_My_Account_Omnichannel') : t('This_option_affect_this_conversation_only_To_set_default_selection_go_to_My_Account_Omnichannel')} - + )} diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx index 82c92d39cc8f..bdbde6b05acd 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx @@ -1,5 +1,16 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Field, FieldGroup, Button, TextAreaInput, Modal, Box, PaginatedSelectFiltered, Divider } from '@rocket.chat/fuselage'; +import { + Field, + FieldGroup, + Button, + TextAreaInput, + Modal, + Box, + PaginatedSelectFiltered, + Divider, + FieldLabel, + FieldRow, +} from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -66,7 +77,7 @@ const ForwardChatModal = ({ const endReached = useCallback( (start) => { - if (departmentsPhase === AsyncStatePhase.LOADING) { + if (departmentsPhase !== AsyncStatePhase.LOADING) { loadMoreDepartments(start, Math.min(50, departmentsTotal)); } }, @@ -102,8 +113,8 @@ const ForwardChatModal = ({ - {t('Forward_to_department')} - + {t('Forward_to_department')} + - + - {t('Forward_to_user')} - + {t('Forward_to_user')} + - + - + {t('Leave_a_comment')}{' '} ({t('Optional')}) - - + + - + diff --git a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx index e879caf38032..2757b5d9a88b 100644 --- a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx @@ -1,5 +1,5 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Field, Button, TextInput, Modal, Box, FieldGroup } from '@rocket.chat/fuselage'; +import { Field, Button, TextInput, Modal, Box, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React, { useCallback, useEffect } from 'react'; @@ -78,28 +78,28 @@ const TranscriptModal: FC = ({ {!!transcriptRequest &&

{t('Livechat_transcript_already_requested_warning')}

} - {t('Email')}* - + {t('Email')}* + - - {errors.email?.message} + + {errors.email?.message} - {t('Subject')}* - + {t('Subject')}* + - - {errors.subject?.message} + + {errors.subject?.message} diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 7026c6cab35f..eb0414e7da14 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -49,8 +49,8 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet filter={filter} setFilter={setFilter} multiple - renderSelected={({ selected: { value, label }, onRemove }): ReactElement => ( - + renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( + {label?.name} diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx index 1b7936cafd3e..19074f7f6b34 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx @@ -1,4 +1,4 @@ -import { Box, FieldGroup, TextInput, Field } from '@rocket.chat/fuselage'; +import { Box, FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react'; @@ -59,13 +59,13 @@ const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername, invalidAttem > - + {t('Verify_your_email_with_the_code_we_sent')} - - + + - - {invalidAttempt && {t('Invalid_password')}} + + {invalidAttempt && {t('Invalid_password')}} diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx index 3d604fc5004b..4c91e274de68 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx @@ -1,4 +1,4 @@ -import { Box, PasswordInput, FieldGroup, Field } from '@rocket.chat/fuselage'; +import { Box, PasswordInput, FieldGroup, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, Ref, SyntheticEvent } from 'react'; @@ -43,10 +43,10 @@ const TwoFactorPasswordModal = ({ onConfirm, onClose, invalidAttempt }: TwoFacto > - + {t('For_your_security_you_must_enter_your_current_password_to_continue')} - - + + } @@ -54,8 +54,8 @@ const TwoFactorPasswordModal = ({ onConfirm, onClose, invalidAttempt }: TwoFacto onChange={onChange} placeholder={t('Password')} > - - {invalidAttempt && {t('Invalid_password')}} + + {invalidAttempt && {t('Invalid_password')}} diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx index 266e92586944..04aa7a4f94f0 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx @@ -1,4 +1,4 @@ -import { Box, TextInput, Field, FieldGroup } from '@rocket.chat/fuselage'; +import { Box, TextInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react'; @@ -42,13 +42,13 @@ const TwoFactorTotpModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorTot > - + {t('Open_your_authentication_app_and_enter_the_code')} - - + + - - {invalidAttempt && {t('Invalid_password')}} + + {invalidAttempt && {t('Invalid_password')}} diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx index 96d11cf44cb7..f96c198865cc 100644 --- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx @@ -18,7 +18,16 @@ const UserAndRoomAutoCompleteMultiple = ({ value, onChange, ...props }: UserAndR const debouncedFilter = useDebouncedValue(filter, 1000); const rooms = useUserSubscriptions( - useMemo(() => ({ open: { $ne: false }, lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') }), [debouncedFilter]), + useMemo( + () => ({ + open: { $ne: false }, + $or: [ + { lowerCaseFName: new RegExp(escapeRegExp(debouncedFilter), 'i') }, + { lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') }, + ], + }), + [debouncedFilter], + ), ).filter((room) => { if (!user) { return; diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx index dfda12cdd2e4..857af5e9c43f 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx @@ -31,7 +31,7 @@ const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultip setFilter={setFilter} onChange={onChange} multiple - renderSelected={({ selected: { value, label }, onRemove }): ReactElement => ( + renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index 4674528a483f..5552e6da0745 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -46,7 +46,7 @@ const MessageContentBody = ({ mentions, channels, md, searchText }: MessageConte text-decoration: underline; } &:focus { - border: 2px solid ${Palette.stroke['stroke-extra-light-highlight']}; + box-shadow: 0 0 0 2px ${Palette.stroke['stroke-extra-light-highlight']}; border-radius: 2px; } } diff --git a/apps/meteor/client/definitions/info.d.ts b/apps/meteor/client/definitions/info.d.ts index 2b66032f484a..43fa1fc53414 100644 --- a/apps/meteor/client/definitions/info.d.ts +++ b/apps/meteor/client/definitions/info.d.ts @@ -23,4 +23,9 @@ declare module '*.info' { tag?: string; branch?: string; }; + + export const minimumClientVersions: { + desktop: string; + mobile: string; + }; } diff --git a/apps/meteor/client/hooks/useAppTranslations.ts b/apps/meteor/client/hooks/useAppTranslations.ts index ad8ca5966c2b..bf4f83e48d85 100644 --- a/apps/meteor/client/hooks/useAppTranslations.ts +++ b/apps/meteor/client/hooks/useAppTranslations.ts @@ -65,7 +65,7 @@ export const useAppTranslations = () => { // Translations keys must be scoped under app id const scopedTranslations = Object.entries(translations).reduce>((acc, [key, value]) => { acc[Utilities.getI18nKeyForApp(key, appId)] = value; - return translations; + return acc; }, {}); i18n.addResourceBundle(normalizedLanguage, 'core', scopedTranslations); diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts index 99b7e5e3461c..0f568d9bd5cc 100644 --- a/apps/meteor/client/hooks/useLicense.ts +++ b/apps/meteor/client/hooks/useLicense.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useLicense = (): UseQueryResult> => { const getLicenses = useEndpoint('GET', '/v1/licenses.get'); + const canViewLicense = usePermission('view-privileged-setting'); - return useQuery(['licenses', 'getLicenses'], () => getLicenses(), { - staleTime: Infinity, - keepPreviousData: true, - }); + return useQuery( + ['licenses', 'getLicenses'], + () => { + if (!canViewLicense) { + throw new Error('unauthorized api call'); + } + return getLicenses(); + }, + { + staleTime: Infinity, + keepPreviousData: true, + }, + ); }; diff --git a/apps/meteor/client/hooks/useRegistrationStatus.ts b/apps/meteor/client/hooks/useRegistrationStatus.ts index 8b091459291b..9260d672bec5 100644 --- a/apps/meteor/client/hooks/useRegistrationStatus.ts +++ b/apps/meteor/client/hooks/useRegistrationStatus.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useRegistrationStatus = (): UseQueryResult> => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); + const canViewregistrationStatus = usePermission('manage-cloud'); - return useQuery(['getRegistrationStatus'], () => getRegistrationStatus(), { - keepPreviousData: true, - staleTime: Infinity, - }); + return useQuery( + ['getRegistrationStatus'], + () => { + if (!canViewregistrationStatus) { + throw new Error('unauthorized api call'); + } + return getRegistrationStatus(); + }, + { + keepPreviousData: true, + staleTime: Infinity, + }, + ); }; diff --git a/apps/meteor/client/hooks/useTimeAgo.ts b/apps/meteor/client/hooks/useTimeAgo.ts index 2b661ae89cf3..724f61d8c7c8 100644 --- a/apps/meteor/client/hooks/useTimeAgo.ts +++ b/apps/meteor/client/hooks/useTimeAgo.ts @@ -14,7 +14,7 @@ export const useTimeAgo = (): ((time: Date | number | string) => string) => { (time) => { return moment(time).calendar(null, { sameDay: format, - lastDay: moment().calendar('lastDay').replace('LT', format), + lastDay: moment(time).calendar('lastDay').replace('LT', format), lastWeek: `dddd ${format}`, sameElse: 'LL', }); diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index e5ff73be77b1..95f7f1dcb361 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -1,10 +1,12 @@ import { SHA256 } from '@rocket.chat/sha256'; import { Meteor } from 'meteor/meteor'; +import { lazy } from 'react'; -import TwoFactorModal from '../../components/TwoFactorModal'; import { imperativeModal } from '../imperativeModal'; import { isTotpInvalidError, isTotpRequiredError } from './utils'; +const TwoFactorModal = lazy(() => import('../../components/TwoFactorModal')); + const twoFactorMethods = ['totp', 'email', 'password'] as const; type TwoFactorMethod = (typeof twoFactorMethods)[number]; diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index bd6e01458863..f2c049ad04b1 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -217,7 +217,6 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const deleteMessage = async (mid: IMessage['_id']): Promise => { await sdk.call('deleteMessage', { _id: mid }); - Messages.remove({ _id: mid }); }; const drafts = new Map(); diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index bb08f0242d4f..334515980787 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -1,13 +1,15 @@ -import '../ee/client/ecdh'; -import './polyfills'; +import { FlowRouter } from 'meteor/kadira:flow-router'; -import '../lib/oauthRedirectUriClient'; -import './lib/meteorCallWrapper'; -import './importPackages'; +FlowRouter.wait(); -import '../ee/client'; -import './methods'; -import './startup'; -import './views/admin'; -import './views/marketplace'; -import './views/account'; +FlowRouter.notFound = { + action: () => undefined, +}; + +import('./polyfills') + .then(() => Promise.all([import('./lib/meteorCallWrapper'), import('../lib/oauthRedirectUriClient')])) + .then(() => import('../ee/client/ecdh')) + .then(() => import('./importPackages')) + .then(() => Promise.all([import('./methods'), import('./startup')])) + .then(() => import('../ee/client')) + .then(() => Promise.all([import('./views/admin'), import('./views/marketplace'), import('./views/account')])); diff --git a/apps/meteor/client/polyfills/index.ts b/apps/meteor/client/polyfills/index.ts index 46f5bcb8d68d..f07d828a4602 100644 --- a/apps/meteor/client/polyfills/index.ts +++ b/apps/meteor/client/polyfills/index.ts @@ -4,4 +4,3 @@ import './childNodeRemove'; import './cssVars'; import './customEventPolyfill'; import './hoverTouchClick'; -import './objectFromEntries'; diff --git a/apps/meteor/client/polyfills/objectFromEntries.ts b/apps/meteor/client/polyfills/objectFromEntries.ts deleted file mode 100644 index d59198ebd1d3..000000000000 --- a/apps/meteor/client/polyfills/objectFromEntries.ts +++ /dev/null @@ -1,5 +0,0 @@ -Object.fromEntries = - Object.fromEntries || - function fromEntries(entries: Iterable): { [k: string]: T } { - return [...entries].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}); - }; diff --git a/apps/meteor/client/providers/RouterProvider.tsx b/apps/meteor/client/providers/RouterProvider.tsx index 0dd7ee31deed..0f146ec83128 100644 --- a/apps/meteor/client/providers/RouterProvider.tsx +++ b/apps/meteor/client/providers/RouterProvider.tsx @@ -17,12 +17,6 @@ import React from 'react'; import { appLayout } from '../lib/appLayout'; import { queueMicrotask } from '../lib/utils/queueMicrotask'; -FlowRouter.wait(); - -FlowRouter.notFound = { - action: () => undefined, -}; - const subscribers = new Set<() => void>(); const listenToRouteChange = () => { diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index fdddb9ec5349..2cf47066c4e4 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -2,7 +2,7 @@ import { useLocalStorage, useMutableCallback } from '@rocket.chat/fuselage-hooks import languages from '@rocket.chat/i18n/dist/languages'; import en from '@rocket.chat/i18n/src/locales/en.i18n.json'; import type { TranslationKey, TranslationContextValue } from '@rocket.chat/ui-contexts'; -import { useMethod, useSetting, TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import { useMethod, useSetting, TranslationContext } from '@rocket.chat/ui-contexts'; import type i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -12,6 +12,7 @@ import React, { useEffect, useMemo } from 'react'; import { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next'; import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; +import { getURL } from '../../app/utils/client'; import { i18n, addSprinfToI18n } from '../../app/utils/lib/i18n'; import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; import { applyCustomTranslations } from '../lib/utils/applyCustomTranslations'; @@ -39,8 +40,6 @@ const parseToJSON = (customTranslations: string): Record>(); const useI18next = (lng: string): typeof i18next => { - const basePath = useAbsoluteUrl()('/i18n'); - const customTranslations = useSetting('Custom_Translations'); const parsedCustomTranslations = useMemo(() => { @@ -79,6 +78,11 @@ const useI18next = (lng: string): typeof i18next => { if (prefix) { result[key.slice(prefix.length + 1)] = value; + continue; + } + + if (Array.isArray(namespaces) ? namespaces.includes('core') : namespaces === 'core') { + result[key] = value; } } } @@ -100,17 +104,18 @@ const useI18next = (lng: string): typeof i18next => { partialBundledLanguages: true, defaultNS: 'core', backend: { - loadPath: `${basePath}/{{lng}}.json`, + loadPath: 'i18n/{{lng}}.json', parse: (data: string, lngs?: string | string[], namespaces: string | string[] = []) => extractKeys(JSON.parse(data), lngs, namespaces), request: (_options, url, _payload, callback) => { const params = url.split('/'); + const lng = params[params.length - 1]; let promise = localeCache.get(lng); if (!promise) { - promise = fetch(url).then((res) => res.text()); + promise = fetch(getURL(url)).then((res) => res.text()); localeCache.set(lng, promise); } diff --git a/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx index 69afd3c2667a..593bd784be90 100644 --- a/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx @@ -1,21 +1,14 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { SidebarSection } from '@rocket.chat/fuselage'; import type { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ComponentType, ReactElement } from 'react'; +import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '../../contexts/VideoConfContext'; import type { useAvatarTemplate } from '../hooks/useAvatarTemplate'; import type { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import OmnichannelSection from '../sections/OmnichannelSection'; import SideBarItemTemplateWithData from './SideBarItemTemplateWithData'; -const sections: { - [key: string]: ComponentType; -} = { - Omnichannel: OmnichannelSection, -}; - type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -44,10 +37,7 @@ const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: ISubscripti ); if (typeof item === 'string') { - const Section = sections[item]; - return Section ? ( -
- ) : ( + return ( {t(item)} diff --git a/apps/meteor/client/sidebar/Sidebar.tsx b/apps/meteor/client/sidebar/Sidebar.tsx index ae333bbdb2a1..84c63eac01be 100644 --- a/apps/meteor/client/sidebar/Sidebar.tsx +++ b/apps/meteor/client/sidebar/Sidebar.tsx @@ -4,12 +4,16 @@ import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; import { useLayout, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; import SidebarRoomList from './RoomList'; import SidebarFooter from './footer'; import SidebarHeader from './header'; +import OmnichannelSection from './sections/OmnichannelSection'; import StatusDisabledSection from './sections/StatusDisabledSection'; const Sidebar = () => { + const showOmnichannel = useOmnichannelEnabled(); + const sidebarViewMode = useUserPreference('sidebarViewMode'); const sidebarHideAvatar = !useUserPreference('sidebarDisplayAvatar'); const { sidebar } = useLayout(); @@ -18,6 +22,9 @@ const Sidebar = () => { const sideBarBackground = css` background-color: ${Palette.surface['surface-tint']}; + a { + text-decoration: none; + } `; return ( @@ -38,6 +45,7 @@ const Sidebar = () => { > {presenceDisabled && !bannerDismissed && setBannerDismissed(true)} />} + {showOmnichannel && } diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index 57ebffdbcf39..3f001b158d72 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -1,4 +1,18 @@ -import { Box, Modal, Button, TextInput, Icon, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; +import { + Box, + Modal, + Button, + TextInput, + Icon, + Field, + ToggleSwitch, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + FieldDescription, +} from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { @@ -194,10 +208,10 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): - + {t('Channel_name')} - - + + - + {errors.name && ( - + {errors.name.message} - + )} - {t('Topic')} - + {t('Topic')} + - - {t('Channel_what_is_this_channel_about')} + + {t('Channel_what_is_this_channel_about')} - {t('Private')} - + {t('Private')} + {isPrivate ? t('Only_invited_users_can_acess_this_channel') : t('Everyone_can_access_this_channel')} - + - {t('Federation_Matrix_Federated')} - {t(getFederationHintKey(federatedModule, federationEnabled))} + {t('Federation_Matrix_Federated')} + {t(getFederationHintKey(federatedModule, federationEnabled))} - {t('Read_only')} - + {t('Read_only')} + {readOnly ? t('Only_authorized_users_can_write_new_messages') : t('All_users_in_the_channel_can_write_new_messages')} - + - {t('Encrypted')} - + {t('Encrypted')} + {isPrivate ? t('Encrypted_channel_Description') : t('Encrypted_not_available')} - + - {t('Broadcast')} - {t('Broadcast_channel_Description')} + {t('Broadcast')} + {t('Broadcast_channel_Description')} - {t('Add_members')} + {t('Add_members')} ; - -type CreateDirectMessageProps = { - onClose: () => void; -}; - -const CreateDirectMessage: FC = ({ onClose }) => { +const CreateDirectMessage = ({ onClose }: { onClose: () => void }) => { const t = useTranslation(); - const [users, setUsers] = useState>([]); + const membersFieldId = useUniqueId(); + const dispatchToastMessage = useToastMessageDispatch(); - const createDirect = useEndpointAction('POST', '/v1/dm.create'); + const createDirectAction = useEndpoint('POST', '/v1/dm.create'); - const onCreate = useMutableCallback(async (e) => { - e.preventDefault(); - if (!users.length) return; - try { - const { - room: { rid }, - } = await createDirect({ usernames: users.join(',') }); + const { + control, + handleSubmit, + formState: { isDirty, isSubmitting, isValidating, errors }, + } = useForm({ mode: 'onBlur', defaultValues: { users: [] } }); + const mutateDirectMessage = useMutation({ + mutationFn: createDirectAction, + onSuccess: ({ room: { rid } }) => { goToRoomById(rid); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { onClose(); - } catch (error) { - console.warn(error); - } + }, }); + const handleCreate = async ({ users }: { users: IUser['username'][] }) => { + return mutateDirectMessage.mutateAsync({ usernames: users.join(',') }); + }; + return ( - ) => } - > + }> - {t('Direct_Messages')} - + {t('Create_direct_message')} + - {t('Direct_message_creation_description')} - - - + {t('Direct_message_creation_description')} + + + + {t('Members')} + + + ( + + )} + /> + + {errors.users && ( + + {errors.users.message} + + )} + + - diff --git a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx index 1f12c2b59b49..e14ef6e77b37 100644 --- a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx @@ -1,15 +1,29 @@ -import { Box, Modal, Button, TextInput, Field, ToggleSwitch, FieldGroup, Icon } from '@rocket.chat/fuselage'; import { - useTranslation, - useSetting, - usePermission, + Box, + Button, + Field, + Icon, + Modal, + TextInput, + ToggleSwitch, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + FieldDescription, +} from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, - useToastMessageDispatch, + usePermission, usePermissionWithScopedRoles, + useSetting, + useToastMessageDispatch, + useTranslation, } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; -import React, { memo, useMemo, useEffect } from 'react'; -import { useForm, Controller } from 'react-hook-form'; +import React, { memo, useEffect, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; @@ -129,18 +143,35 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => } }; + const createTeamFormId = useUniqueId(); + const nameId = useUniqueId(); + const topicId = useUniqueId(); + const privateId = useUniqueId(); + const readOnlyId = useUniqueId(); + const encryptedId = useUniqueId(); + const broadcastId = useUniqueId(); + const addMembersId = useUniqueId(); + return ( - ) => }> + ) => ( + + )} + > - {t('Teams_New_Title')} + {t('Teams_New_Title')} - {t('Teams_New_Name_Label')} - + + {t('Teams_New_Name_Label')} + + void }): ReactElement => placeholder={t('Team_Name')} addon={} error={errors.name?.message} + aria-describedby={`${nameId}-error`} + aria-required='true' /> - - {errors?.name && {errors.name.message}} + + {errors?.name && ( + + {errors.name.message} + + )} - + {t('Teams_New_Description_Label')}{' '} ({t('optional')}) - - - - + + + + - {t('Teams_New_Private_Label')} - + {t('Teams_New_Private_Label')} + {isPrivate ? t('Teams_New_Private_Description_Enabled') : t('Teams_New_Private_Description_Disabled')} - + ( - + )} /> @@ -184,16 +226,23 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => - {t('Teams_New_Read_only_Label')} - + {t('Teams_New_Read_only_Label')} + {readOnly ? t('Only_authorized_users_can_write_new_messages') : t('Teams_New_Read_only_Description')} - + ( - + )} /> @@ -201,16 +250,23 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => - {t('Teams_New_Encrypted_Label')} - + {t('Teams_New_Encrypted_Label')} + {isPrivate ? t('Teams_New_Encrypted_Description_Enabled') : t('Teams_New_Encrypted_Description_Disabled')} - + ( - + )} /> @@ -218,25 +274,25 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => - {t('Teams_New_Broadcast_Label')} - {t('Teams_New_Broadcast_Description')} + {t('Teams_New_Broadcast_Label')} + {t('Teams_New_Broadcast_Description')} ( - + )} /> - + {t('Teams_New_Add_members_Label')}{' '} ({t('optional')}) - + - {t('StatusMessage')} - + {t('StatusMessage')} + } /> - - {!allowUserStatusMessageChange && {t('StatusMessage_Change_Disabled')}} - {statusTextError} + + {!allowUserStatusMessageChange && {t('StatusMessage_Change_Disabled')}} + {statusTextError} diff --git a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx index a0f90a7d2f2f..6ea69ce6c037 100644 --- a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx +++ b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx @@ -1,4 +1,16 @@ -import { Divider, Modal, ButtonGroup, Button, Field, TextInput, Throbber } from '@rocket.chat/fuselage'; +import { + Divider, + Modal, + ButtonGroup, + Button, + Field, + TextInput, + Throbber, + FieldLabel, + FieldRow, + FieldError, + FieldHint, +} from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetModal, useTranslation, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -63,8 +75,8 @@ const MatrixFederationAddServerModal: VFC = - {t('Server_name')} - + {t('Server_name')} + = {!isLoading && t('Add')} {isLoading && } - - {isError && errorKey && {t(errorKey)}} - {t('Federation_Example_matrix_server')} + + {isError && errorKey && {t(errorKey)}} + {t('Federation_Example_matrix_server')} {!isLoadingServerList && data?.servers && } diff --git a/apps/meteor/client/sidebar/header/UserMenu.tsx b/apps/meteor/client/sidebar/header/UserMenu.tsx index 9fcc7a0d2274..a53836eda311 100644 --- a/apps/meteor/client/sidebar/header/UserMenu.tsx +++ b/apps/meteor/client/sidebar/header/UserMenu.tsx @@ -24,6 +24,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} @@ -36,6 +37,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } medium + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx index 248b91418739..b0b20972d346 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx @@ -19,12 +19,14 @@ it('should not show upgrade item if has license and not have trial', async () => workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); await waitFor(() => !!(result.all.length > 1)); - expect(result.current).toEqual([]); + expect(result.current.length).toEqual(1); }); it('should return an upgrade item if not have license or if have a trial', async () => { @@ -42,10 +44,13 @@ it('should return an upgrade item if not have license or if have a trial', async workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); - await waitFor(() => !!result.current[0]); + // Workspace admin is also expected to be here + await waitFor(() => result.current.length > 1); expect(result.current[0]).toEqual( expect.objectContaining({ diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx index 20006cf01588..646b85c838be 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx @@ -25,19 +25,19 @@ export const useGroupingListItems = (): GenericMenuItemProps[] => { id: 'unread', content: t('Unread'), icon: 'flag', - addon: , + addon: , }, { id: 'favorites', content: t('Favorites'), icon: 'star', - addon: , + addon: , }, { id: 'types', content: t('Types'), icon: 'group-by-type', - addon: , + addon: , }, ]; }; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx index b9432f821373..56041ab4e571 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx @@ -26,14 +26,14 @@ export const useSortModeItems = (): GenericMenuItemProps[] => { id: 'activity', content: t('Activity'), icon: 'clock', - addon: , + addon: , description: sidebarSortBy === 'activity' && isOmnichannelEnabled && , }, { id: 'name', content: t('Name'), icon: 'sort-az', - addon: , + addon: , description: sidebarSortBy === 'alphabetical' && isOmnichannelEnabled && , }, ]; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx index 3e27dd22c7fa..ca2855d09db5 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx @@ -29,25 +29,25 @@ export const useViewModeItems = (): GenericMenuItemProps[] => { id: 'extended', content: t('Extended'), icon: 'extended-view', - addon: , + addon: , }, { id: 'medium', content: t('Medium'), icon: 'medium-view', - addon: , + addon: , }, { id: 'condensed', content: t('Condensed'), icon: 'condensed-view', - addon: , + addon: , }, { id: 'avatars', content: t('Avatars'), icon: 'user-rounded', - addon: , + addon: , }, ]; }; diff --git a/apps/meteor/client/sidebar/hooks/useRoomList.ts b/apps/meteor/client/sidebar/hooks/useRoomList.ts index 436c7c1dc71d..fa5dfd2797cb 100644 --- a/apps/meteor/client/sidebar/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebar/hooks/useRoomList.ts @@ -92,7 +92,6 @@ export const useRoomList = (): Array => { }); const groups = new Map(); - showOmnichannel && groups.set('Omnichannel', []); incomingCall.size && groups.set('Incoming Calls', incomingCall); showOmnichannel && inquiries.enabled && queue.length && groups.set('Incoming_Livechats', queue); showOmnichannel && omnichannel.size && groups.set('Open_Livechats', omnichannel); diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx index 542fa05c54ab..e7dec5f3506a 100644 --- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx +++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx @@ -1,15 +1,13 @@ -import type { Box } from '@rocket.chat/fuselage'; import { Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useLayout, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import React, { memo } from 'react'; import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; import { useOmnichannelShowQueueLink } from '../../hooks/omnichannel/useOmnichannelShowQueueLink'; import { OmniChannelCallDialPad, OmnichannelCallToggle, OmnichannelLivechatToggle } from './actions'; -const OmnichannelSection = (props: typeof Box): ReactElement => { +const OmnichannelSection = () => { const t = useTranslation(); const isCallEnabled = useIsCallEnabled(); const isCallReady = useIsCallReady(); @@ -34,7 +32,7 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { // The className is a paliative while we make TopBar.ToolBox optional on fuselage return ( - + {t('Omnichannel')} {showOmnichannelQueueLink && ( @@ -56,6 +54,4 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { ); }; -export default Object.assign(memo(OmnichannelSection), { - size: 56, -}); +export default memo(OmnichannelSection); diff --git a/apps/meteor/client/startup/rootUrlChange.ts b/apps/meteor/client/startup/rootUrlChange.ts index 45f98634a373..4e42874eba4a 100644 --- a/apps/meteor/client/startup/rootUrlChange.ts +++ b/apps/meteor/client/startup/rootUrlChange.ts @@ -6,6 +6,8 @@ import { Roles } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; +import FingerprintChangeModal from '../components/FingerprintChangeModal'; +import FingerprintChangeModalConfirmation from '../components/FingerprintChangeModalConfirmation'; import UrlChangeModal from '../components/UrlChangeModal'; import { imperativeModal } from '../lib/imperativeModal'; import { dispatchToastMessage } from '../lib/toast'; @@ -58,3 +60,72 @@ Meteor.startup(() => { return c.stop(); }); }); + +Meteor.startup(() => { + Tracker.autorun((c) => { + const userId = Meteor.userId(); + if (!userId) { + return; + } + + if (!Roles.ready.get() || !isSyncReady.get()) { + return; + } + + if (hasRole(userId, 'admin') === false) { + return c.stop(); + } + + const deploymentFingerPrintVerified = settings.get('Deployment_FingerPrint_Verified'); + if (deploymentFingerPrintVerified == null || deploymentFingerPrintVerified === true) { + return; + } + + const updateWorkspace = (): void => { + imperativeModal.close(); + void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'updated-configuration' }).then(() => { + dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') }); + }); + }; + + const setNewWorkspace = (): void => { + imperativeModal.close(); + void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'new-workspace' }).then(() => { + dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') }); + }); + }; + + const openModal = (): void => { + imperativeModal.open({ + component: FingerprintChangeModal, + props: { + onConfirm: () => { + imperativeModal.open({ + component: FingerprintChangeModalConfirmation, + props: { + onConfirm: setNewWorkspace, + onCancel: openModal, + newWorkspace: true, + }, + }); + }, + onCancel: () => { + imperativeModal.open({ + component: FingerprintChangeModalConfirmation, + props: { + onConfirm: updateWorkspace, + onCancel: openModal, + newWorkspace: false, + }, + }); + }, + onClose: imperativeModal.close, + }, + }); + }; + + openModal(); + + return c.stop(); + }); +}); diff --git a/apps/meteor/client/startup/startup.ts b/apps/meteor/client/startup/startup.ts index 440b55ce5e6d..6b2b66ec69d7 100644 --- a/apps/meteor/client/startup/startup.ts +++ b/apps/meteor/client/startup/startup.ts @@ -72,11 +72,11 @@ Meteor.startup(() => { } const { - registrationStatus: { connectToCloud, workspaceRegistered }, + registrationStatus: { workspaceRegistered }, } = await sdk.rest.get('/v1/cloud.registrationStatus'); c.stop(); - if (connectToCloud === true && workspaceRegistered !== true) { + if (workspaceRegistered !== true) { banners.open({ id: 'cloud-registration', title: () => t('Cloud_registration_pending_title'), diff --git a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx index ceb904613533..657548d5a1b9 100644 --- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx +++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx @@ -38,8 +38,9 @@ const AccessibilityPage = () => { const t = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); - const { data: license } = useIsEnterprise(); const preferencesValues = useAccessiblityPreferencesValues(); + const { data: license } = useIsEnterprise(); + const isEnterprise = license?.isEnterprise; const { themeAppearence } = preferencesValues; const [, setPrevTheme] = useLocalStorage('prevTheme', themeAppearence); @@ -102,14 +103,14 @@ const AccessibilityPage = () => { {themes.map(({ id, title, description, ...item }, index) => { - const communityDisabled = 'isEEOnly' in item && item.isEEOnly && !license?.isEnterprise; + const showCommunityUpsellTriggers = 'isEEOnly' in item && item.isEEOnly && !isEnterprise; return ( {t.has(title) ? t(title) : title} - {communityDisabled && ( + {showCommunityUpsellTriggers && ( @@ -123,7 +124,7 @@ const AccessibilityPage = () => { control={control} name='themeAppearence' render={({ field: { onChange, value, ref } }) => { - if (communityDisabled) { + if (showCommunityUpsellTriggers) { return ( { {t('Mentions_with_@_symbol')} - - - - {t('Enterprise')} - - + {!isEnterprise && ( + + + + {t('Enterprise')} + + + )} - {license?.isEnterprise ? ( + {isEnterprise ? ( void }) => if (!isAdmin) { return ( ); } return ( { - {t(feature.i18n)} - + {t(feature.i18n)} + - + - {feature.description && {t(feature.description)}} + {feature.description && {t(feature.description)}} {feature.imageUrl && } diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx index 71d025d792cd..54806d879aa1 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx @@ -1,6 +1,6 @@ import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { SelectLegacy, Box, Field, Button } from '@rocket.chat/fuselage'; +import { SelectLegacy, Box, Button, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -38,8 +38,8 @@ const AccountIntegrationsPage = (): ReactElement => { - {t('WebDAV_Accounts')} - + {t('WebDAV_Accounts')} + { - + diff --git a/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx index fb1903806fec..9fccaef4593e 100644 --- a/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx +++ b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx @@ -1,4 +1,4 @@ -import { Accordion, Box, Field, FieldGroup, Tag, ToggleSwitch } from '@rocket.chat/fuselage'; +import { Accordion, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldHint, Tag, ToggleSwitch } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -24,7 +24,7 @@ const PreferencesConversationTranscript = () => { - + {t('Omnichannel_transcript_pdf')} @@ -32,16 +32,16 @@ const PreferencesConversationTranscript = () => { {!canSendTranscriptPDF && hasLicense && {t('No_permission')}} - - + + - + - {t('Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description')} + {t('Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description')} - + {t('Omnichannel_transcript_email')} {!canSendTranscriptEmail && ( @@ -50,16 +50,16 @@ const PreferencesConversationTranscript = () => { )} - - + + - + - {t('Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description')} + {t('Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description')} diff --git a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx index 4e616072d185..d09492fdc5a6 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, FieldGroup, MultiSelect } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldGroup, FieldLabel, FieldRow, MultiSelect } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -18,8 +18,8 @@ const PreferencesGlobalSection = () => { - {t('Dont_ask_me_again_list')} - + {t('Dont_ask_me_again_list')} + { )} /> - + diff --git a/apps/meteor/client/views/account/preferences/PreferencesHighlightsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesHighlightsSection.tsx index 8c05a92bad47..85bfc9072b12 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesHighlightsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesHighlightsSection.tsx @@ -1,4 +1,4 @@ -import { Accordion, Field, FieldGroup, TextAreaInput } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldGroup, FieldLabel, FieldRow, FieldHint, TextAreaInput } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -14,11 +14,11 @@ const PreferencesHighlightsSection = () => { - {t('Highlights_List')} - + {t('Highlights_List')} + - - {t('Highlights_How_To')} + + {t('Highlights_How_To')} diff --git a/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx index 2faaa9da89c2..0dc6e25f1ac3 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, Select, FieldGroup } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldGroup, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useLanguages, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; @@ -23,8 +23,8 @@ const PreferencesLocalizationSection = () => { - {t('Language')} - + {t('Language')} + { )} /> - - {t('Enter_Behaviour_Description')} + + {t('Enter_Behaviour_Description')} diff --git a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx index 0775b07cec3e..0b82d7441323 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx @@ -1,4 +1,4 @@ -import { Accordion, Field, FieldGroup, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldGroup, FieldRow, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; @@ -77,7 +77,7 @@ const PreferencesMyDataSection = () => { - + - + diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index e0a6017e7efe..2643eab0ebc7 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, Select, FieldGroup, ToggleSwitch, Button, Box } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldLabel, FieldRow, FieldHint, Select, FieldGroup, ToggleSwitch, Button, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPreference, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; @@ -92,8 +92,8 @@ const PreferencesNotificationsSection = () => { - {t('Desktop_Notifications')} - + {t('Desktop_Notifications')} + {notificationsPermission === 'denied' && t('Desktop_Notifications_Disabled')} {notificationsPermission === 'granted' && ( <> @@ -109,12 +109,12 @@ const PreferencesNotificationsSection = () => { )} - + - {t('Notification_RequireInteraction')} - + {t('Notification_RequireInteraction')} + { /> )} /> - + - {t('Only_works_with_chrome_version_greater_50')} + {t('Only_works_with_chrome_version_greater_50')} - {t('Notification_Desktop_Default_For')} - + {t('Notification_Desktop_Default_For')} + { )} /> - + - {t('Email_Notification_Mode')} - + {t('Email_Notification_Mode')} + { /> )} /> - - + + {canChangeEmailNotification && t('You_need_to_verifiy_your_email_address_to_get_notications')} {!canChangeEmailNotification && t('Email_Notifications_Change_Disabled')} - + {showNewLoginEmailPreference && ( - {t('Receive_Login_Detection_Emails')} - + {t('Receive_Login_Detection_Emails')} + { /> )} /> - + - {t('Receive_Login_Detection_Emails_Description')} + {t('Receive_Login_Detection_Emails_Description')} )} {showCalendarPreference && ( - {t('Notify_Calendar_Events')} - + {t('Notify_Calendar_Events')} + { )} /> - + )} {showMobileRinging && ( - {t('VideoConf_Mobile_Ringing')} - + {t('VideoConf_Mobile_Ringing')} + { )} /> - + )} diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 7b5f051fc017..223d67fd7f95 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, Select, FieldGroup, ToggleSwitch, Tooltip, Box } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldLabel, FieldRow, Select, FieldGroup, ToggleSwitch, Tooltip, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useCustomSound } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,8 +23,8 @@ const PreferencesSoundSection = () => { - {t('New_Room_Notification')} - + {t('New_Room_Notification')} + { /> )} /> - + - {t('New_Message_Notification')} - + {t('New_Message_Notification')} + { /> )} /> - + - {t('Mute_Focused_Conversations')} - + {t('Mute_Focused_Conversations')} + { )} /> - + - {t('Notifications_Sound_Volume')} - + {t('Notifications_Sound_Volume')} + { {notificationsSoundVolume} - + diff --git a/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx index 3a075c50f7d7..89575e36aa6b 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx @@ -1,4 +1,4 @@ -import { Accordion, Field, NumberInput, FieldGroup, ToggleSwitch, Box } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldLabel, FieldRow, NumberInput, FieldGroup, ToggleSwitch, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -16,8 +16,8 @@ const PreferencesUserPresenceSection = () => { - {t('Enable_Auto_Away')} - + {t('Enable_Auto_Away')} + { )} /> - + - {t('Idle_Time_Limit')} - + {t('Idle_Time_Limit')} + - + diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 1a326db4544d..ed97b95caae8 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -1,5 +1,17 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Field, FieldGroup, TextInput, TextAreaInput, Box, Icon, Button } from '@rocket.chat/fuselage'; +import { + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + TextInput, + TextAreaInput, + Box, + Icon, + Button, +} from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { @@ -139,10 +151,10 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle - + {t('Name')} - - + + ): ReactEle /> )} /> - + {errors.name && ( - + {errors.name.message} - + )} - {!allowRealNameChange && {t('RealName_Change_Disabled')}} + {!allowRealNameChange && {t('RealName_Change_Disabled')}} - + {t('Username')} - - + + ): ReactEle /> )} /> - + {errors?.username && ( - + {errors.username.message} - + )} - {!canChangeUsername && {t('Username_Change_Disabled')}} + {!canChangeUsername && {t('Username_Change_Disabled')}} - {t('StatusMessage')} - + {t('StatusMessage')} + ): ReactEle /> )} /> - + {errors?.statusText && ( - + {errors?.statusText.message} - + )} - {!allowUserStatusMessageChange && {t('StatusMessage_Change_Disabled')}} + {!allowUserStatusMessageChange && {t('StatusMessage_Change_Disabled')}} - {t('Nickname')} - + {t('Nickname')} + ): ReactEle } /> )} /> - + - {t('Bio')} - + {t('Bio')} + ): ReactEle /> )} /> - + {errors?.bio && ( - + {errors.bio.message} - + )} - + {t('Email')} - - + + ): ReactEle {t('Resend_verification_email')} )} - + {errors.email && ( - + {errors?.email?.message} - + )} - {!allowEmailChange && {t('Email_Change_Disabled')}} + {!allowEmailChange && {t('Email_Change_Disabled')}} {customFieldsMetadata && } diff --git a/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx b/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx index 4d73f04c2973..286bd564dd18 100644 --- a/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx +++ b/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx @@ -1,4 +1,4 @@ -import { Box, PasswordInput, TextInput, FieldGroup, Field } from '@rocket.chat/fuselage'; +import { Box, PasswordInput, TextInput, FieldGroup, Field, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React, { useState, useCallback } from 'react'; @@ -50,11 +50,11 @@ const ActionConfirmModal: FC = ({ isPassword, onConfirm {isPassword ? t('Enter_your_password_to_delete_your_account') : t('Enter_your_username_to_delete_your_account')} - + {isPassword && } {!isPassword && } - - {inputError} + + {inputError} diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 78410e519e2c..72213f3202ba 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,4 +1,4 @@ -import { Box, Margins, PasswordInput, Field, FieldGroup, Button } from '@rocket.chat/fuselage'; +import { Box, Margins, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation, useLogout } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { useCallback, useEffect } from 'react'; @@ -71,20 +71,20 @@ const EndToEnd = (props: ComponentProps): ReactElement => { - {t('New_encryption_password')} - + {t('New_encryption_password')} + - - {!keysExist && {t('EncryptionKey_Change_Disabled')}} + + {!keysExist && {t('EncryptionKey_Change_Disabled')}} {hasTypedPassword && ( - {t('Confirm_new_encryption_password')} + {t('Confirm_new_encryption_password')} ): ReactElement => { placeholder={t('Confirm_New_Password_Placeholder')} aria-labelledby='Confirm_new_encryption_password' /> - {errors.passwordConfirm && {errors.passwordConfirm.message}} + {errors.passwordConfirm && {errors.passwordConfirm.message}} )} diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index 4d38d2e68fc6..97d2a8163cab 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx @@ -1,4 +1,4 @@ -import { Box, TextInput, Button, Field, FieldGroup, Margins, CheckBox } from '@rocket.chat/fuselage'; +import { Box, TextInput, Button, Field, FieldGroup, FieldLabel, FieldRow, Margins, CheckBox } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -56,18 +56,18 @@ const AddToken = ({ reload, ...props }: { reload: () => void }): ReactElement => return ( - + - - + + - {t('Ignore_Two_Factor_Authentication')} - + {t('Ignore_Two_Factor_Authentication')} + ); diff --git a/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx b/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx index 694d437de8d3..a75e66ec1e4b 100644 --- a/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx +++ b/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx @@ -16,7 +16,6 @@ const RegisterWorkspace = () => { const { data: registrationStatusData, isLoading, isError, refetch } = useRegistrationStatus(); const isWorkspaceRegistered = registrationStatusData?.registrationStatus?.workspaceRegistered ?? false; - const isConnectedToCloud = registrationStatusData?.registrationStatus?.connectToCloud ?? false; if (isLoading || isError) { return null; @@ -40,32 +39,11 @@ const RegisterWorkspace = () => { setModal(); }; - const handleRegistrationTag = () => { - if (!isWorkspaceRegistered && !isConnectedToCloud) { - return {t('RegisterWorkspace_NotRegistered_Title')}; - } - if (isWorkspaceRegistered && !isConnectedToCloud) { - return {t('RegisterWorkspace_NotConnected_Title')}; - } - return {t('Workspace_registered')}; - }; - - const handleCardsTitle = () => { - if (!isWorkspaceRegistered && !isConnectedToCloud) { - return t('RegisterWorkspace_NotRegistered_Subtitle'); - } - if (isWorkspaceRegistered && !isConnectedToCloud) { - return t('RegisterWorkspace_NotConnected_Subtitle'); - } - return t('RegisterWorkspace_Registered_Description'); - }; - return ( { - {handleRegistrationTag()} + + {!isWorkspaceRegistered && {t('RegisterWorkspace_NotRegistered_Title')}} + {isWorkspaceRegistered && {t('Workspace_registered')}} + - - {handleCardsTitle()} + + {!isWorkspaceRegistered && t('RegisterWorkspace_NotRegistered_Subtitle')} + {isWorkspaceRegistered && t('RegisterWorkspace_Registered_Description')} diff --git a/apps/meteor/client/views/admin/cloud/components/RegisterWorkspaceMenu.tsx b/apps/meteor/client/views/admin/cloud/components/RegisterWorkspaceMenu.tsx index d5b0ec7cf771..9163e5e30af5 100644 --- a/apps/meteor/client/views/admin/cloud/components/RegisterWorkspaceMenu.tsx +++ b/apps/meteor/client/views/admin/cloud/components/RegisterWorkspaceMenu.tsx @@ -8,7 +8,6 @@ import RegisteredWorkspaceModal from '../modals/RegisteredWorkspaceModal'; type RegisterWorkspaceMenuProps = { isWorkspaceRegistered: boolean | string; - isConnectedToCloud: boolean | string; onClick: () => void; onClickOfflineRegistration: () => void; onStatusChange?: () => void; @@ -16,7 +15,6 @@ type RegisterWorkspaceMenuProps = { const RegisterWorkspaceMenu = ({ isWorkspaceRegistered, - isConnectedToCloud, onClick, onClickOfflineRegistration, onStatusChange, @@ -33,7 +31,7 @@ const RegisterWorkspaceMenu = ({ return ( - {isWorkspaceRegistered && isConnectedToCloud && ( + {isWorkspaceRegistered && ( <> - )} {!isWorkspaceRegistered && ( <> diff --git a/apps/meteor/client/views/admin/cloud/modals/DisconnectWorkspaceModal.tsx b/apps/meteor/client/views/admin/cloud/modals/DisconnectWorkspaceModal.tsx deleted file mode 100644 index 44846ffe24c5..000000000000 --- a/apps/meteor/client/views/admin/cloud/modals/DisconnectWorkspaceModal.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Box, Button, ButtonGroup, Modal } from '@rocket.chat/fuselage'; -import { useMethod, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import useFeatureBullets from '../hooks/useFeatureBullets'; -import RegisteredWorkspaceModal from './RegisteredWorkspaceModal'; - -type DisconnectWorkspaceModalProps = { - onClose: () => void; - onStatusChange?: () => void; -}; - -const DisconnectWorkspaceModal = ({ onClose, onStatusChange, ...props }: DisconnectWorkspaceModalProps) => { - const t = useTranslation(); - const setModal = useSetModal(); - const bulletFeatures = useFeatureBullets(); - const dispatchToastMessage = useToastMessageDispatch(); - - const disconnectWorkspace = useMethod('cloud:disconnectWorkspace'); - - const handleCancelAction = (): void => { - const handleModalClose = (): void => setModal(null); - setModal(); - }; - - const handleUnregister = async () => { - try { - const success = await disconnectWorkspace(); - - if (!success) { - throw Error(t('RegisterWorkspace_Disconnect_Error')); - } - - dispatchToastMessage({ type: 'success', message: t('Disconnected') }); - - setModal(null); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - await (onStatusChange && onStatusChange()); - } - }; - - return ( - - - - {t('Are_you_sure')} - - - - - - {`${t('RegisterWorkspace_Disconnect_Subtitle')}: `} -
    - {bulletFeatures.map((item, index) => ( -
  • - {item.title} - - {item.disconnect} - -
  • - ))} -
-
-
- - - - - - -
- ); -}; - -export default DisconnectWorkspaceModal; diff --git a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepOneModal.tsx b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepOneModal.tsx index 09548b3349a9..41e8f300d898 100644 --- a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepOneModal.tsx +++ b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepOneModal.tsx @@ -1,4 +1,4 @@ -import { Modal, Box, Field, TextInput, CheckBox, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldLabel, FieldRow, TextInput, CheckBox, ButtonGroup, Button } from '@rocket.chat/fuselage'; import { ExternalLink } from '@rocket.chat/ui-client'; import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -66,14 +66,14 @@ const RegisterWorkspaceSetupStepOneModal = ({ {t('RegisterWorkspace_Setup_Subtitle')}
- {t('RegisterWorkspace_Setup_Label')} - + {t('RegisterWorkspace_Setup_Label')} + { setEmail((e.target as HTMLInputElement).value); }} /> - + diff --git a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx index fa1640bc8dfb..734f07442f11 100644 --- a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx +++ b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx @@ -1,4 +1,4 @@ -import { Modal, Box, Field, TextInput } from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage'; import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect } from 'react'; import { Trans } from 'react-i18next'; @@ -81,10 +81,10 @@ const RegisterWorkspaceSetupStepTwoModal = ({ email, step, setStep, onClose, int {t('RegisterWorkspace_Setup_Email_Verification')} - {t('Security_code')} - + {t('Security_code')} + - +
diff --git a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceTokenModal.tsx b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceTokenModal.tsx index 12ba3798c97e..89728457226b 100644 --- a/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceTokenModal.tsx +++ b/apps/meteor/client/views/admin/cloud/modals/RegisterWorkspaceTokenModal.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonGroup, Field, Modal, TextInput } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup, Field, FieldLabel, FieldRow, FieldError, Modal, TextInput } from '@rocket.chat/fuselage'; import { useMethod, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; import React, { useState } from 'react'; @@ -79,11 +79,11 @@ const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: Regi {`2. ${t('RegisterWorkspace_Token_Step_Two')}`} - {t('Registration_Token')} - + {t('Registration_Token')} + - - {error && {t('Token_Not_Recognized')}} + + {error && {t('Token_Not_Recognized')}} diff --git a/apps/meteor/client/views/admin/cloud/modals/RegisteredWorkspaceModal.tsx b/apps/meteor/client/views/admin/cloud/modals/RegisteredWorkspaceModal.tsx index 1a77893548f2..050411075c74 100644 --- a/apps/meteor/client/views/admin/cloud/modals/RegisteredWorkspaceModal.tsx +++ b/apps/meteor/client/views/admin/cloud/modals/RegisteredWorkspaceModal.tsx @@ -4,7 +4,6 @@ import { useMethod, useSetModal, useToastMessageDispatch, useTranslation } from import React, { useState } from 'react'; import useFeatureBullets from '../hooks/useFeatureBullets'; -import DisconnectWorkspaceModal from './DisconnectWorkspaceModal'; type RegisteredWorkspaceModalProps = { onClose: () => void; @@ -20,11 +19,6 @@ const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: Registe const syncWorkspace = useMethod('cloud:syncWorkspace'); - const handleDisconnect = (): void => { - const handleModalClose = (): void => setModal(null); - setModal(); - }; - const handleSyncAction = async () => { setSyncing(true); @@ -40,7 +34,7 @@ const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: Registe } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } finally { - await (onStatusChange && onStatusChange()); + onStatusChange?.(); setSyncing(false); } }; @@ -70,9 +64,6 @@ const RegisteredWorkspaceModal = ({ onClose, onStatusChange, ...props }: Registe - diff --git a/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx index eaeaccd3e395..8fae8501327a 100644 --- a/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, FieldError, Icon } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; import React, { useCallback, useState } from 'react'; @@ -78,28 +78,28 @@ const AddCustomEmoji = ({ close, onChange, ...props }: AddCustomEmojiProps): Rea <> - {t('Name')} - + {t('Name')} + - - {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} + + {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} - {t('Aliases')} - + {t('Aliases')} + - - {errors.aliases && {t('Custom_Emoji_Error_Same_Name_And_Alias')}} + + {errors.aliases && {t('Custom_Emoji_Error_Same_Name_And_Alias')}} - + {t('Custom_Emoji')} {/* FIXME: replace to IconButton */} - - {errors.emoji && {t('error-the-field-is-required', { field: t('Custom_Emoji') })}} + + {errors.emoji && {t('error-the-field-is-required', { field: t('Custom_Emoji') })}} {newEmojiPreview && ( diff --git a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx index e11b5722fe0a..f561001a20be 100644 --- a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx @@ -1,4 +1,16 @@ -import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldGroup, IconButton } from '@rocket.chat/fuselage'; +import { + Box, + Button, + ButtonGroup, + Margins, + TextInput, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + IconButton, +} from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useAbsoluteUrl, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, ChangeEvent } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; @@ -134,24 +146,24 @@ const EditCustomEmoji: FC = ({ close, onChange, data, ...p - {t('Name')} - + {t('Name')} + - - {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} + + {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} - {t('Aliases')} - + {t('Aliases')} + - - {errors.aliases && {t('Custom_Emoji_Error_Same_Name_And_Alias')}} + + {errors.aliases && {t('Custom_Emoji_Error_Same_Name_And_Alias')}} - + {t('Custom_Emoji')} - + {newEmojiPreview && ( diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index bf391e8feae1..434cb1769db6 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -1,4 +1,4 @@ -import { Field, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, FormEvent } from 'react'; import React, { useState, useCallback } from 'react'; @@ -86,17 +86,17 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr <> - {t('Name')} - + {t('Name')} + ): void => setName(e.currentTarget.value)} placeholder={t('Name')} /> - + - {t('Sound_File_mp3')} + {t('Sound_File_mp3')} {/* FIXME: replace to IconButton */} diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index d1d214d15e20..44b728821c4c 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonGroup, Margins, TextInput, Field, IconButton } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, IconButton } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, SyntheticEvent } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; @@ -120,17 +120,17 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl <> - {t('Name')} - + {t('Name')} + ): void => setName(e.currentTarget.value)} placeholder={t('Name')} /> - + - {t('Sound_File_mp3')} + {t('Sound_File_mp3')} diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx index 2c832e8299ec..9796438fde7e 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx @@ -1,6 +1,6 @@ import type { IUserStatus } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { FieldGroup, Button, ButtonGroup, TextInput, Field, Select } from '@rocket.chat/fuselage'; +import { FieldGroup, Button, ButtonGroup, TextInput, Field, FieldLabel, FieldRow, FieldError, Select } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useRoute, useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -92,23 +92,23 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor - {t('Name')} - + {t('Name')} + - - {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} + + {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} - {t('Presence')} - + {t('Presence')} + + + {t('Script_Engine_Description')} + + ), + [scriptEngine, scriptEngineOptions, handleScriptEngine, t], + )} {useMemo( () => ( diff --git a/apps/meteor/client/views/admin/integrations/OutgoiongWebhookForm.js b/apps/meteor/client/views/admin/integrations/OutgoiongWebhookForm.js index 09705be52134..b8d9d8e13a6b 100644 --- a/apps/meteor/client/views/admin/integrations/OutgoiongWebhookForm.js +++ b/apps/meteor/client/views/admin/integrations/OutgoiongWebhookForm.js @@ -26,6 +26,7 @@ export default function OutgoingWebhookForm({ formValues, formHandlers, append, token, scriptEnabled, script, + scriptEngine, retryFailedCalls, retryCount, retryDelay, @@ -48,6 +49,7 @@ export default function OutgoingWebhookForm({ formValues, formHandlers, append, handleEmoji, handleToken, handleScriptEnabled, + handleScriptEngine, handleScript, handleRetryFailedCalls, handleRetryCount, @@ -66,6 +68,13 @@ export default function OutgoingWebhookForm({ formValues, formHandlers, append, ); const eventOptions = useMemo(() => Object.entries(outgoingEvents).map(([key, val]) => [key, t(val.label)]), [t]); + const scriptEngineOptions = useMemo( + () => [ + ['vm2', t('Script_Engine_vm2')], + ['isolated-vm', t('Script_Engine_isolated_vm')], + ], + [t], + ); const showChannel = useMemo(() => outgoingEvents[event].use.channel, [event]); const showTriggerWords = useMemo(() => outgoingEvents[event].use.triggerWords, [event]); @@ -290,6 +299,18 @@ export default function OutgoingWebhookForm({ formValues, formHandlers, append, ), [handleScriptEnabled, scriptEnabled, t], )} + {useMemo( + () => ( + + {t('Script_Engine')} + + [extension, extension]) || []} @@ -61,7 +61,7 @@ const AssignAgentModal: FC = ({ existingExtension, close placeholder={t('Select_an_option')} onChange={(value) => setExtension(String(value))} /> - + diff --git a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx index bd9de953eb30..b674e589d446 100644 --- a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx @@ -1,4 +1,4 @@ -import { Button, Field } from '@rocket.chat/fuselage'; +import { Button, FieldRow, FieldHint } from '@rocket.chat/fuselage'; import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -29,12 +29,12 @@ function ActionSettingInput({ _id, actionText, value, disabled, sectionChanged } return ( <> - + - - {sectionChanged && {t('Save_to_enable_this_action')}} + + {sectionChanged && {t('Save_to_enable_this_action')}} ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx index 5871296cee6a..f3122a23295b 100644 --- a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx @@ -1,4 +1,4 @@ -import { Button, Field, Icon } from '@rocket.chat/fuselage'; +import { Button, FieldLabel, FieldRow, Icon } from '@rocket.chat/fuselage'; import { Random } from '@rocket.chat/random'; import { useToastMessageDispatch, useEndpoint, useTranslation, useUpload } from '@rocket.chat/ui-contexts'; import type { ChangeEventHandler, DragEvent, ReactElement, SyntheticEvent } from 'react'; @@ -58,10 +58,10 @@ function AssetSettingInput({ _id, label, value, asset, fileConstraints }: AssetS return ( <> - + {label} - - + +
{value?.url ? (
- + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx index acac16b5b7e8..308e781e8e46 100644 --- a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx @@ -1,4 +1,4 @@ -import { Field, ToggleSwitch } from '@rocket.chat/fuselage'; +import { FieldLabel, FieldRow, ToggleSwitch } from '@rocket.chat/fuselage'; import type { ReactElement, SyntheticEvent } from 'react'; import React from 'react'; @@ -30,7 +30,7 @@ function BooleanSettingInput({ }; return ( - + - + {label} - + {hasResetButton && } - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx index 2b615a63b8d5..85698b66e2b7 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldHint, Flex } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -43,12 +43,12 @@ function CodeSettingInput({ <> - + {label} - + {hasResetButton && } - {hint && {hint}} + {hint && {hint}} - + {label} - + {hasResetButton && } - + {editor === 'color' && ( @@ -104,9 +104,9 @@ function ColorSettingInput({ options={allowedTypes.map((type) => [type, t(type)])} /> - + - Variable name: {_id.replace(/theme-color-/, '@')} + Variable name: {_id.replace(/theme-color-/, '@')} ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx index 2d7a93b9c96f..35b255b4e756 100644 --- a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, TextInput } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, TextInput } from '@rocket.chat/fuselage'; import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; @@ -36,13 +36,13 @@ function FontSettingInput({ <> - + {label} - + {hasResetButton && } - + - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx index 46ffc61a8f3b..32425a57c698 100644 --- a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, TextInput } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, TextInput } from '@rocket.chat/fuselage'; import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; @@ -36,13 +36,13 @@ function GenericSettingInput({ <> - + {label} - + {hasResetButton && } - + - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx index 03f837117223..cd5abd54f481 100644 --- a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, InputBox } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, InputBox } from '@rocket.chat/fuselage'; import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; @@ -37,13 +37,13 @@ function IntSettingInput({ <> - + {label} - + {hasResetButton && } - + - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx index 770edd5c12a6..8bfe977aaf39 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, Select } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, Select } from '@rocket.chat/fuselage'; import { useLanguages } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -40,13 +40,13 @@ function LanguageSettingInput({ <> - + {label} - + {hasResetButton && } - + handleChange(String(value))} options={values.map(({ key, label }) => [key, label])} /> - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx index 114503c959ed..bc8d8062d8a2 100644 --- a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx @@ -1,4 +1,4 @@ -import { Field, Flex, Box, MultiSelectFiltered, MultiSelect } from '@rocket.chat/fuselage'; +import { FieldLabel, Flex, Box, MultiSelectFiltered, MultiSelect } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -45,9 +45,9 @@ function MultiSelectSettingInput({ <> - + {label} - + {hasResetButton && } diff --git a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx index 087d83b98a0d..b7d2c1d48d47 100644 --- a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, PasswordInput } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, PasswordInput } from '@rocket.chat/fuselage'; import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; @@ -37,13 +37,13 @@ function PasswordSettingInput({ <> - + {label} - + {hasResetButton && } - + - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx index 0541ea81eb36..b94581706757 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, UrlInput } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, Flex, UrlInput } from '@rocket.chat/fuselage'; import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; @@ -40,9 +40,9 @@ function RelativeUrlSettingInput({ <> - + {label} - + {hasResetButton && } diff --git a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx index d44235afbde9..15423742ff91 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx @@ -1,5 +1,5 @@ import type { SettingValueRoomPick } from '@rocket.chat/core-typings'; -import { Box, Field, Flex } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -42,13 +42,13 @@ function RoomPickSettingInput({ <> - + {label} - + {hasResetButton && } - + - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx index 28014d65d375..a6fa88f7ffe7 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, Select } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, Select } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -43,13 +43,13 @@ function SelectSettingInput({ <> - + {label} - + {hasResetButton && } - + handleChange(String(value))} options={moment.tz.names().map((key) => [key, key])} /> - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx index 30b79e01683f..3d0ba78a127a 100644 --- a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx @@ -1,4 +1,4 @@ -import { Box, Field, Flex, TextAreaInput, TextInput } from '@rocket.chat/fuselage'; +import { Box, FieldLabel, FieldRow, Flex, TextAreaInput, TextInput } from '@rocket.chat/fuselage'; import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; @@ -43,13 +43,13 @@ function StringSettingInput({ <> - + {label} - + {hasResetButton && } - + {multiline ? ( )} - + ); } diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index c397e28e6db1..2beee76cee02 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -112,8 +112,8 @@ export const { permissionGranted: (): boolean => hasPermission('run-import'), }, { - href: '/admin/logs', - i18nLabel: 'Logs', + href: '/admin/reports', + i18nLabel: 'Reports', icon: 'post', permissionGranted: (): boolean => hasPermission('view-logs'), }, diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 334aca68b8f8..1912150b4a48 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -1,6 +1,10 @@ import type { AvatarObject, IUser, Serialized } from '@rocket.chat/core-typings'; import { Field, + FieldLabel, + FieldRow, + FieldError, + FieldHint, TextInput, TextAreaInput, PasswordInput, @@ -179,8 +183,8 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - {t('Name')} - + {t('Name')} + { /> )} /> - + {errors?.name && ( - + {errors.name.message} - + )} - {t('Username')} - + {t('Username')} + { /> )} /> - + {errors?.username && ( - + {errors.username.message} - + )} - {t('Email')} - + {t('Email')} + { /> )} /> - + {errors?.email && ( - + {errors.email.message} - + )} - {t('Verified')} - + {t('Verified')} + } /> - + - {t('StatusMessage')} - + {t('StatusMessage')} + { /> )} /> - + {errors?.statusText && ( - + {errors.statusText.message} - + )} - {t('Bio')} - + {t('Bio')} + { /> )} /> - + {errors?.bio && ( - + {errors.bio.message} - + )} - {t('Nickname')} - + {t('Nickname')} + { } /> )} /> - + {!setRandomPassword && ( - {t('Password')} - + {t('Password')} + { /> )} /> - + {errors?.password && ( - + {errors.password.message} - + )} )} - {t('Require_password_change')} - + {t('Require_password_change')} + { /> )} /> - + - {t('Set_random_password_and_send_by_email')} - + {t('Set_random_password_and_send_by_email')} + { /> )} /> - + {!isSmtpEnabled && ( - )} - {t('Roles')} - + {t('Roles')} + {roleError && {roleError}} {!roleError && ( { )} /> )} - - {errors?.roles && {errors.roles.message}} + + {errors?.roles && {errors.roles.message}} - {t('Join_default_channels')} - + {t('Join_default_channels')} + { )} /> - + - {t('Send_welcome_email')} - + {t('Send_welcome_email')} + { /> )} /> - + {!isSmtpEnabled && ( - diff --git a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx new file mode 100644 index 000000000000..cd300c14a481 --- /dev/null +++ b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx @@ -0,0 +1,41 @@ +import { Box, Icon, Skeleton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { useAnalyticsObject } from './hooks/useAnalyticsObject'; + +const AnalyticsReports = () => { + const t = useTranslation(); + + const { data, isLoading, isSuccess, isError } = useAnalyticsObject(); + + return ( + <> + + + + + + {t('How_and_why_we_collect_usage_data')} + + + {t('Analytics_page_briefing_first_paragraph')} + + {t('Analytics_page_briefing_second_paragraph')} + + + {isSuccess &&
{JSON.stringify(data, null, '\t')}
} + {isError && t('Something_went_wrong_try_again_later')} + {isLoading && ( + <> + + + + + )} +
+ + ); +}; + +export default AnalyticsReports; diff --git a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx index a75c22da19b0..2c1613f3ef74 100644 --- a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx +++ b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx @@ -1,18 +1,30 @@ +import { Tabs } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import Page from '../../../components/Page'; +import AnalyticsReports from './AnalyticsReports'; import ServerLogs from './ServerLogs'; const ViewLogsPage = (): ReactElement => { const t = useTranslation(); + const [tab, setTab] = useState('Logs'); + return ( - + - + + setTab('Logs')} selected={tab === 'Logs'}> + {t('Logs')} + + setTab('Analytics')} selected={tab === 'Analytics'}> + {t('Analytic_reports')} + + + {tab === 'Logs' ? : } ); diff --git a/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts b/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts new file mode 100644 index 000000000000..8aad0e605964 --- /dev/null +++ b/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts @@ -0,0 +1,8 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useAnalyticsObject = () => { + const getAnalytics = useEndpoint('GET', '/v1/statistics'); + + return useQuery(['analytics'], () => getAnalytics({}), { staleTime: 10 * 60 * 1000 }); +}; diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx index 76d5f3b61e28..249ae6df5efa 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx @@ -1,4 +1,4 @@ -import { Box, PasswordInput, Field, FieldGroup } from '@rocket.chat/fuselage'; +import { Box, PasswordInput, Field, FieldGroup, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -50,7 +50,7 @@ const EnterE2EPasswordModal = ({ - + - - {passwordError} + + {passwordError} diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index e051b69db8fa..65dd4cb1e396 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,3 +1,4 @@ +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; @@ -16,9 +17,12 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - const trialLicense = licensesData?.licenses?.find(({ meta }) => meta?.trial); - const isTrial = licensesData?.licenses?.every(({ meta }) => meta?.trial) ?? false; - const trialEndDate = trialLicense?.meta ? format(new Date(trialLicense.meta.trialEnd), 'yyyy-MM-dd') : undefined; + const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; + + const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); + const isTrial = Boolean(trialLicense); + const trialEndDateStr = trialLicense?.information?.visualExpiration || trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd; + const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined; const upgradeTabType = getUpgradeTabType({ registered, diff --git a/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx b/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx index 2fdbffda7d4d..f1332284c97f 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx @@ -50,7 +50,7 @@ const AppsFilters = ({ const appsSearchPlaceholders: { [key: string]: string } = { explore: t('Search_Apps'), - enterprise: t('Search_Enterprise_Apps'), + enterprise: t('Search_Premium_Apps'), installed: t('Search_Installed_Apps'), requested: t('Search_Requested_Apps'), private: t('Search_Private_apps'), diff --git a/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx b/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx index 40d90b56e046..1bc7642e5afc 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx @@ -40,7 +40,7 @@ const AppsPageContent = (): ReactElement => { { id: 'all', label: t('All_Prices'), checked: true }, { id: 'free', label: t('Free_Apps'), checked: false }, { id: 'paid', label: t('Paid_Apps'), checked: false }, - { id: 'enterprise', label: t('Enterprise'), checked: false }, + { id: 'premium', label: t('Premium'), checked: false }, ], }); const freePaidFilterOnSelected = useRadioToggle(setFreePaidFilterStructure); @@ -89,7 +89,7 @@ const AppsPageContent = (): ReactElement => { const getAppsData = useCallback((): appsDataType => { switch (context) { - case 'enterprise': + case 'premium': case 'explore': case 'requested': return marketplaceApps; @@ -100,6 +100,24 @@ const AppsPageContent = (): ReactElement => { } }, [context, marketplaceApps, installedApps, privateApps]); + const findSort = () => { + const possibleSort = sortFilterStructure.items.find(({ checked }) => checked); + + return possibleSort ? possibleSort.id : 'mru'; + }; + + const findPurchaseType = () => { + const possiblePurchaseType = freePaidFilterStructure.items.find(({ checked }) => checked); + + return possiblePurchaseType ? possiblePurchaseType.id : 'all'; + }; + + const findStatus = () => { + const possibleStatus = statusFilterStructure.items.find(({ checked }) => checked); + + return possibleStatus ? possibleStatus.id : 'all'; + }; + const [categories, selectedCategories, categoryTagList, onSelected] = useCategories(); const appsResult = useFilteredApps({ appsData: getAppsData(), @@ -107,9 +125,9 @@ const AppsPageContent = (): ReactElement => { current, itemsPerPage, categories: useMemo(() => selectedCategories.map(({ label }) => label), [selectedCategories]), - purchaseType: useMemo(() => freePaidFilterStructure.items.find(({ checked }) => checked)?.id, [freePaidFilterStructure]), - sortingMethod: useMemo(() => sortFilterStructure.items.find(({ checked }) => checked)?.id, [sortFilterStructure]), - status: useMemo(() => statusFilterStructure.items.find(({ checked }) => checked)?.id, [statusFilterStructure]), + purchaseType: useMemo(findPurchaseType, [freePaidFilterStructure]), + sortingMethod: useMemo(findSort, [sortFilterStructure]), + status: useMemo(findStatus, [statusFilterStructure]), context, }); diff --git a/apps/meteor/client/views/marketplace/BundleChips.tsx b/apps/meteor/client/views/marketplace/BundleChips.tsx index 9f988534fe14..4e2953bd8519 100644 --- a/apps/meteor/client/views/marketplace/BundleChips.tsx +++ b/apps/meteor/client/views/marketplace/BundleChips.tsx @@ -18,15 +18,15 @@ const BundleChips = ({ bundledIn }: BundleChipsProps): ReactElement => { return ( <> - {bundledIn.map((bundle) => ( + {bundledIn.map(({ bundleId, bundleName }) => ( - {bundle.bundleName} + {bundleName} ))} diff --git a/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx b/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx index 72cfa5474346..da135cbdedfc 100644 --- a/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx +++ b/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx @@ -15,7 +15,7 @@ const EnabledAppsCount = ({ percentage: number; limit: number; enabled: number; - context: 'private' | 'explore' | 'installed' | 'enterprise' | 'requested'; + context: 'private' | 'explore' | 'installed' | 'premium' | 'requested'; }): ReactElement | null => { const t = useTranslation(); diff --git a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx index f93cb1fcd339..7696801c3124 100644 --- a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx +++ b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx @@ -12,7 +12,7 @@ import EnabledAppsCount from './EnabledAppsCount'; const MarketplaceHeader = ({ title }: { title: string }): ReactElement | null => { const t = useTranslation(); const isAdmin = usePermission('manage-apps'); - const context = (useRouteParameter('context') || 'explore') as 'private' | 'explore' | 'installed' | 'enterprise' | 'requested'; + const context = (useRouteParameter('context') || 'explore') as 'private' | 'explore' | 'installed' | 'premium' | 'requested'; const route = useRoute('marketplace'); const setModal = useSetModal(); const result = useAppsCountQuery(context); diff --git a/apps/meteor/client/views/marketplace/hooks/useAppInfo.ts b/apps/meteor/client/views/marketplace/hooks/useAppInfo.ts index 7e69cdeef97c..44ab240ce7b3 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppInfo.ts +++ b/apps/meteor/client/views/marketplace/hooks/useAppInfo.ts @@ -36,7 +36,7 @@ export const useAppInfo = (appId: string, context: string): AppInfo | undefined } let appResult: App | undefined; - const marketplaceAppsContexts = ['explore', 'enterprise', 'requested']; + const marketplaceAppsContexts = ['explore', 'premium', 'requested']; if (marketplaceAppsContexts.includes(context)) appResult = marketplaceApps.value?.apps.find((app) => app.id === appId); diff --git a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts index 10689c773479..a571eac3b71f 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts +++ b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts @@ -11,10 +11,10 @@ const getProgressBarValues = (numberOfEnabledApps: number, enabledAppsLimit: num percentage: Math.round((numberOfEnabledApps / enabledAppsLimit) * 100), }); -export type MarketplaceRouteContext = 'private' | 'explore' | 'installed' | 'enterprise' | 'requested'; +export type MarketplaceRouteContext = 'private' | 'explore' | 'installed' | 'premium' | 'requested'; export function isMarketplaceRouteContext(context: string): context is MarketplaceRouteContext { - return ['private', 'explore', 'installed', 'enterprise', 'requested'].includes(context); + return ['private', 'explore', 'installed', 'premium', 'requested'].includes(context); } export const useAppsCountQuery = (context: MarketplaceRouteContext) => { diff --git a/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts b/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts index 19664cd4b693..437c8d35207d 100644 --- a/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts +++ b/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts @@ -33,11 +33,11 @@ export const useFilteredApps = ({ text: string; current: number; itemsPerPage: number; - categories?: string[]; - purchaseType?: string; + categories: string[]; + purchaseType: string; isEnterpriseOnly?: boolean; - sortingMethod?: string; - status?: string; + sortingMethod: string; + status: string; context?: string; }): AsyncState< { items: App[] } & { shouldShowSearchText: boolean } & PaginatedResult & { allApps: App[] } & { totalAppsLength: number } @@ -48,72 +48,58 @@ export const useFilteredApps = ({ } const { apps } = appsData.value; - - let filtered: App[] = apps; - let shouldShowSearchText = true; - - const sortingMethods: Record App[]> = { - urf: () => - filtered.sort( - (firstApp, secondApp) => (secondApp?.appRequestStats?.totalUnseen || 0) - (firstApp?.appRequestStats?.totalUnseen || 0), - ), - url: () => - filtered.sort( - (firstApp, secondApp) => (firstApp?.appRequestStats?.totalUnseen || 0) - (secondApp?.appRequestStats?.totalUnseen || 0), - ), - az: () => filtered.sort((firstApp, secondApp) => sortAppsByAlphabeticalOrInverseOrder(firstApp.name, secondApp.name)), - za: () => filtered.sort((firstApp, secondApp) => sortAppsByAlphabeticalOrInverseOrder(secondApp.name, firstApp.name)), - mru: () => - filtered.sort((firstApp, secondApp) => sortAppsByClosestOrFarthestModificationDate(firstApp.modifiedAt, secondApp.modifiedAt)), - lru: () => - filtered.sort((firstApp, secondApp) => sortAppsByClosestOrFarthestModificationDate(secondApp.modifiedAt, firstApp.modifiedAt)), + const fallback = (apps: App[]) => apps; + + const sortingMethods: Record App[]> = { + urf: (apps: App[]) => + apps.sort((firstApp, secondApp) => (secondApp?.appRequestStats?.totalUnseen || 0) - (firstApp?.appRequestStats?.totalUnseen || 0)), + url: (apps: App[]) => + apps.sort((firstApp, secondApp) => (firstApp?.appRequestStats?.totalUnseen || 0) - (secondApp?.appRequestStats?.totalUnseen || 0)), + az: (apps: App[]) => apps.sort((firstApp, secondApp) => sortAppsByAlphabeticalOrInverseOrder(firstApp.name, secondApp.name)), + za: (apps: App[]) => apps.sort((firstApp, secondApp) => sortAppsByAlphabeticalOrInverseOrder(secondApp.name, firstApp.name)), + mru: (apps: App[]) => + apps.sort((firstApp, secondApp) => sortAppsByClosestOrFarthestModificationDate(firstApp.modifiedAt, secondApp.modifiedAt)), + lru: (apps: App[]) => + apps.sort((firstApp, secondApp) => sortAppsByClosestOrFarthestModificationDate(secondApp.modifiedAt, firstApp.modifiedAt)), }; - if (context && context === 'enterprise') { - filtered = apps.filter(({ categories }) => categories.includes('Enterprise')); - } - - if (sortingMethod) { - filtered = sortingMethods[sortingMethod](); - } - - const filterByPurchaseType: Record App[]> = { - paid: () => filtered.filter(filterAppsByPaid), - enterprise: () => filtered.filter(filterAppsByEnterprise), - free: () => filtered.filter(filterAppsByFree), + const filterByPurchaseType: Record App[]> = { + all: fallback, + paid: (apps: App[]) => apps.filter(filterAppsByPaid), + premium: (apps: App[]) => apps.filter(filterAppsByEnterprise), + free: (apps: App[]) => apps.filter(filterAppsByFree), }; - if (purchaseType && purchaseType !== 'all') { - filtered = filterByPurchaseType[purchaseType](); - - if (!filtered.length) shouldShowSearchText = false; - } - - if (status && status !== 'all') { - filtered = status === 'enabled' ? filtered.filter(filterAppsByEnabled) : filtered.filter(filterAppsByDisabled); - - if (!filtered.length) shouldShowSearchText = false; - } - - if (Boolean(categories.length) && Boolean(text)) { - filtered = filtered.filter((app) => filterAppsByCategories(app, categories)).filter(({ name }) => filterAppsByText(name, text)); - shouldShowSearchText = true; - } - - if (Boolean(categories.length) && !text) { - filtered = filtered.filter((app) => filterAppsByCategories(app, categories)); - shouldShowSearchText = false; - } - - if (!categories.length && Boolean(text)) { - filtered = filtered.filter(({ name }) => filterAppsByText(name, text)); - shouldShowSearchText = true; - } + const filterByStatus: Record App[]> = { + all: fallback, + enabled: (apps: App[]) => apps.filter(filterAppsByEnabled), + disabled: (apps: App[]) => apps.filter(filterAppsByDisabled), + }; - if (context && context === 'requested') { - filtered = apps.filter(({ appRequestStats, installed }) => Boolean(appRequestStats) && !installed); - } + const filterByContext: Record App[]> = { + explore: fallback, + installed: fallback, + private: fallback, + premium: (apps: App[]) => apps.filter(({ categories }) => categories.includes('Premium')), + requested: (apps: App[]) => apps.filter(({ appRequestStats, installed }) => Boolean(appRequestStats) && !installed), + }; + type appsFilterFunction = (apps: App[]) => App[]; + const pipeAppsFilter = + (...functions: appsFilterFunction[]) => + (initialValue: App[]) => + functions.reduce((currentAppsList, currentFilterFunction) => currentFilterFunction(currentAppsList), initialValue); + + const filtered = pipeAppsFilter( + context ? filterByContext[context] : fallback, + filterByPurchaseType[purchaseType], + filterByStatus[status], + categories.length ? (apps: App[]) => apps.filter((app) => filterAppsByCategories(app, categories)) : fallback, + text ? (apps: App[]) => apps.filter(({ name }) => filterAppsByText(name, text)) : fallback, + sortingMethods[sortingMethod], + )(apps); + + const shouldShowSearchText = !!text; const total = filtered.length; const offset = current > total ? 0 : current; const end = current + itemsPerPage; diff --git a/apps/meteor/client/views/marketplace/sidebarItems.tsx b/apps/meteor/client/views/marketplace/sidebarItems.tsx index bafcc4e62c58..f829cccf3238 100644 --- a/apps/meteor/client/views/marketplace/sidebarItems.tsx +++ b/apps/meteor/client/views/marketplace/sidebarItems.tsx @@ -17,9 +17,9 @@ export const { permissionGranted: (): boolean => hasAtLeastOnePermission(['access-marketplace', 'manage-apps']), }, { - href: '/marketplace/enterprise', + href: '/marketplace/premium', icon: 'lightning', - i18nLabel: 'Enterprise', + i18nLabel: 'Premium', permissionGranted: (): boolean => hasAtLeastOnePermission(['access-marketplace', 'manage-apps']), }, { diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index b6b536925ba1..aa5a917405b4 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -1,5 +1,17 @@ import type { ILivechatAgent, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; -import { Field, TextInput, Button, Box, MultiSelect, Icon, Select, ContextualbarFooter, ButtonGroup } from '@rocket.chat/fuselage'; +import { + Field, + FieldLabel, + FieldRow, + TextInput, + Button, + Box, + MultiSelect, + Icon, + Select, + ContextualbarFooter, + ButtonGroup, +} from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useSetting, useMethod, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { FC, ReactElement } from 'react'; @@ -121,26 +133,26 @@ const AgentEdit: FC = ({ data, userDepartments, availableDepartm )} - {t('Name')} - + {t('Name')} + - + - {t('Username')} - + {t('Username')} + } /> - + - {t('Email')} - + {t('Email')} + } /> - + - {t('Departments')} - + {t('Departments')} + = ({ data, userDepartments, availableDepartm placeholder={t('Select_an_option')} onChange={handleDepartments} /> - + - {t('Status')} - + {t('Status')} + setChartName(String(value))} /> - + diff --git a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx index b84996f168d4..40644b1f1b04 100644 --- a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx @@ -1,4 +1,4 @@ -import { Box, InputBox, Menu, Field } from '@rocket.chat/fuselage'; +import { Box, InputBox, Menu, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { Moment } from 'moment'; @@ -112,21 +112,21 @@ const DateRangePicker = ({ onChange = () => undefined, ...props }: DateRangePick - {t('Start')} - + {t('Start')} + - + - {t('End')} - + {t('End')} + - + diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx index 9d5e176301a8..4983c5ca8837 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx @@ -1,4 +1,16 @@ -import { Box, Field, TextInput, ToggleSwitch, Accordion, FieldGroup, InputBox, TextAreaInput, NumberInput } from '@rocket.chat/fuselage'; +import { + Box, + Field, + FieldLabel, + FieldRow, + TextInput, + ToggleSwitch, + Accordion, + FieldGroup, + InputBox, + TextAreaInput, + NumberInput, +} from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, FormEvent } from 'react'; @@ -105,46 +117,46 @@ const AppearanceForm: FC = ({ values = {}, handlers = {} }) - {t('Title')} - + {t('Title')} + - + - {t('Title_bar_color')} - + {t('Title_bar_color')} + - + - {t('Message_Characther_Limit')} - + {t('Message_Characther_Limit')} + - + - + - + - {t('Show_agent_info')} - + {t('Show_agent_info')} + - + - {t('Show_agent_email')} - + {t('Show_agent_email')} + - + @@ -154,66 +166,66 @@ const AppearanceForm: FC = ({ values = {}, handlers = {} }) - {t('Display_offline_form')} - + {t('Display_offline_form')} + - + - {t('Offline_form_unavailable_message')} - + {t('Offline_form_unavailable_message')} + - + - {t('Offline_message')} - + {t('Offline_message')} + - + - {t('Title_offline')} - + {t('Title_offline')} + - + - {t('Title_bar_color_offline')} - + {t('Title_bar_color_offline')} + - + - {t('Email_address_to_send_offline_messages')} - + {t('Email_address_to_send_offline_messages')} + - + - {t('Offline_success_message')} - + {t('Offline_success_message')} + - + @@ -222,64 +234,64 @@ const AppearanceForm: FC = ({ values = {}, handlers = {} }) - {t('Enabled')} - + {t('Enabled')} + - + - {t('Show_name_field')} - + {t('Show_name_field')} + - + - {t('Show_email_field')} - + {t('Show_email_field')} + - + - {t('Livechat_registration_form_message')} - + {t('Livechat_registration_form_message')} + - + - {t('Conversation_finished_message')} - + {t('Conversation_finished_message')} + - + - {t('Conversation_finished_text')} - + {t('Conversation_finished_text')} + - + diff --git a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx index 5c40b36417ce..955c1918be05 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx @@ -11,7 +11,7 @@ const ContactHistory = () => { return ( <> {chatId && chatId !== '' ? ( - + ) : ( )} diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index 1c3a79f30f9e..d82498ff1e50 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -117,7 +117,7 @@ const currentChatQuery: useQueryType = ( return query; }; -const CurrentChatsRoute = ({ id, onRowClick }: { id?: string; onRowClick: (_id: string) => void }): ReactElement => { +const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: string) => void }): ReactElement => { const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'priorityWeight' | 'ts' | 'lm' | 'open'>( 'ts', 'desc', @@ -347,4 +347,4 @@ const CurrentChatsRoute = ({ id, onRowClick }: { id?: string; onRowClick: (_id: ); }; -export default memo(CurrentChatsRoute); +export default memo(CurrentChatsPage); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 7d2694d242c9..842a1ad6227d 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -26,7 +26,7 @@ const CurrentChatsRoute = (): ReactElement => { } // TODO: Missing error state - return ; + return ; }; export default memo(CurrentChatsRoute); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx index e0b8a77381b4..803b74b9522d 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx @@ -1,5 +1,5 @@ import type { ILivechatCustomField } from '@rocket.chat/core-typings'; -import { Field, TextInput, Select } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, TextInput, Select } from '@rocket.chat/fuselage'; import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; import type { ReactElement, Dispatch, SetStateAction } from 'react'; import React, { useEffect } from 'react'; @@ -39,8 +39,8 @@ const CustomFieldsList = ({ setCustomFields, allCustomFields }: CustomFieldsList if (customField.type === 'select') { return ( - {customField.label} - + {customField.label} + [item, item])} /> )} /> - + ); } return ( - {customField.label} - + {customField.label} + - + ); })} diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx index ce7780181ddc..f9872c710a5e 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx @@ -43,7 +43,9 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) ); const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields'); - const { data, isSuccess, isLoading, refetch } = useQuery(['livechat-customFields', query], async () => getCustomFields(query)); + const { data, isSuccess, isLoading, refetch } = useQuery(['livechat-customFields', query, debouncedFilter], async () => + getCustomFields(query), + ); const [defaultQuery] = useState(hashQueryKey([query])); const queryHasChanged = defaultQuery !== hashQueryKey([query]); @@ -105,7 +107,7 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) {isSuccess && data.customFields.length > 0 && ( <> - + {headers} {data.customFields.map(({ label, _id, scope, visibility }) => ( diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx index b1542f857316..cead6a0fb15f 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx @@ -1,4 +1,4 @@ -import { Button, Chip, Field, TextInput } from '@rocket.chat/fuselage'; +import { Button, Chip, FieldRow, FieldHint, TextInput } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; import React, { useCallback, useState } from 'react'; @@ -28,7 +28,7 @@ export const DepartmentTags = ({ error, value: tags, onChange }: DepartmentTagsP return ( <> - + {t('Add')} - + - {t('Conversation_closing_tags_description')} + {t('Conversation_closing_tags_description')} {tags?.length > 0 && ( - + {tags.map((tag, i) => ( {tag} ))} - + )} ); diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx index 3a408bfa7507..bb1d5b057437 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx @@ -2,6 +2,9 @@ import type { ILivechatDepartment, ILivechatDepartmentAgents, Serialized } from import { FieldGroup, Field, + FieldLabel, + FieldRow, + FieldError, TextInput, Box, Icon, @@ -12,10 +15,10 @@ import { Button, PaginatedSelectFiltered, } from '@rocket.chat/fuselage'; -import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -127,10 +130,13 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen } = useForm({ mode: 'onChange', defaultValues: initialValues }); const requestTagBeforeClosingChat = watch('requestTagBeforeClosingChat'); - const offlineMessageChannelName = watch('offlineMessageChannelName'); + + const [fallbackFilter, setFallbackFilter] = useState(''); + + const debouncedFallbackFilter = useDebouncedValue(fallbackFilter, 500); const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( - useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), + useMemo(() => ({ text: debouncedFallbackFilter }), [debouncedFallbackFilter]), ); const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); @@ -240,16 +246,16 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen > - {t('Enabled')} - + {t('Enabled')} + - + - {t('Name')}* - + {t('Name')}* + - - {errors.name && {errors.name?.message}} + + {errors.name && {errors.name?.message}} - {t('Description')} - + {t('Description')} + - + - {t('Show_on_registration_page')} - + {t('Show_on_registration_page')} + - + - {t('Email')}* - + {t('Email')}* + validateEmail(email) || t('error-invalid-email-address'), })} /> - - {errors.email && {errors.email?.message}} + + {errors.email && {errors.email?.message}} - {t('Show_on_offline_page')} - + {t('Show_on_offline_page')} + - + - {t('Livechat_DepartmentOfflineMessageToChannel')} - + {t('Livechat_DepartmentOfflineMessageToChannel')} + void} options={roomsItems} placeholder={t('Channel_name')} endReached={ roomsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => loadMoreRooms(start, Math.min(50, roomsTotal)) } + aria-busy={fallbackFilter !== debouncedFallbackFilter} /> )} /> - + {MaxChats && ( @@ -421,7 +428,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen {AutoCompleteDepartment && ( - {t('Fallback_forward_department')} + {t('Fallback_forward_department')} - {t('Request_tag_before_closing_chat')} - + {t('Request_tag_before_closing_chat')} + - + {requestTagBeforeClosingChat && ( - {t('Conversation_closing_tags')}* + {t('Conversation_closing_tags')}* )} /> - {errors.chatClosingTags && {errors.chatClosingTags?.message}} + {errors.chatClosingTags && {errors.chatClosingTags?.message}} )} @@ -475,7 +482,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen - {t('Agents')}: + {t('Agents')}: diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx index 7f5389d0fbe7..64016d23db23 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx @@ -22,7 +22,7 @@ const EditDepartmentWithData = ({ id, title }: EditDepartmentWithDataProps) => { }); if (isInitialLoading) { - return ; + return ; } if (isError || (id && !data?.department)) { diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx index 433af2135068..89b7b4582fa5 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx @@ -1,5 +1,5 @@ import type { ILivechatVisitor, IOmnichannelRoom, Serialized } from '@rocket.chat/core-typings'; -import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; @@ -127,10 +127,10 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps) )} - {t('Topic')} - + {t('Topic')} + - + diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx index afb609e5f801..a41c155c4f9d 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx @@ -1,5 +1,5 @@ import type { ILivechatVisitor, Serialized } from '@rocket.chat/core-typings'; -import { Field, TextInput, ButtonGroup, Button, ContextualbarContent } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError, TextInput, ButtonGroup, Button, ContextualbarContent } from '@rocket.chat/fuselage'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; @@ -189,25 +189,25 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement <> - {t('Name')}* - + {t('Name')}* + - - {errors.name?.message} + + {errors.name?.message} - {t('Email')} - + {t('Email')} + - - {errors.email?.message} + + {errors.email?.message} - {t('Phone')} - + {t('Phone')} + - - {errors.phone?.message} + + {errors.phone?.message} {canViewCustomFields() && } {ContactManager && } diff --git a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx index d6668d7f35d7..b4f56f78b62b 100644 --- a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx +++ b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx @@ -1,4 +1,4 @@ -import { Button, Box, Field } from '@rocket.chat/fuselage'; +import { Button, Box, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -34,13 +34,13 @@ const AddManager = ({ reload }: { reload: () => void }): ReactElement => { return ( - {t('Username')} - + {t('Username')} + - + ); diff --git a/apps/meteor/client/views/omnichannel/sidebarItems.ts b/apps/meteor/client/views/omnichannel/sidebarItems.ts index 048bcb4ef88e..7942764a8b89 100644 --- a/apps/meteor/client/views/omnichannel/sidebarItems.ts +++ b/apps/meteor/client/views/omnichannel/sidebarItems.ts @@ -13,12 +13,6 @@ export const { i18nLabel: 'Current_Chats', permissionGranted: (): boolean => hasPermission('view-livechat-current-chats'), }, - { - href: '/omnichannel/reports', - icon: 'file', - i18nLabel: 'Reports', - permissionGranted: (): boolean => hasPermission('view-livechat-reports'), - }, { href: '/omnichannel/analytics', icon: 'dashboard', diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersForm.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersForm.tsx index 72c8d30d973e..7c9476059411 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersForm.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersForm.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Box, Field, TextInput, ToggleSwitch, Select, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, FieldError, TextInput, ToggleSwitch, Select, TextAreaInput } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, FC, FormEvent } from 'react'; @@ -138,55 +138,55 @@ const TriggersForm: FC = ({ values, handlers, className }) => <> - {t('Enabled')} - + {t('Enabled')} + - + - {t('Run_only_once_for_each_visitor')} - + {t('Run_only_once_for_each_visitor')} + - + - {t('Name')}* - + {t('Name')}* + - - {nameError} + + {nameError} - {t('Description')} - + {t('Description')} + - + - {t('Condition')} - + {t('Condition')} + = ({ values, handlers, className }) => onChange={handleActionSender} placeholder={t('Select_an_option')} /> - + {actionSender === 'custom' && ( - + - + )} - + - - {msgError} + + {msgError} ); diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx index 8a1a81d9f64a..5ade384582c2 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx @@ -1,7 +1,7 @@ import type { ILivechatTrigger } from '@rocket.chat/core-typings'; import { IconButton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useRoute, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; import GenericModal from '../../../components/GenericModal'; @@ -13,7 +13,7 @@ const TriggersRow = ({ _id, name, description, enabled, reload }: TriggersRowPro const t = useTranslation(); const setModal = useSetModal(); const triggersRoute = useRoute('omnichannel-triggers'); - const deleteTrigger = useMethod('livechat:removeTrigger'); + const deleteTrigger = useEndpoint('DELETE', '/v1/livechat/triggers/:_id', { _id }); const dispatchToastMessage = useToastMessageDispatch(); const handleClick = useMutableCallback(() => { @@ -35,7 +35,7 @@ const TriggersRow = ({ _id, name, description, enabled, reload }: TriggersRowPro e.stopPropagation(); const onDeleteTrigger = async () => { try { - await deleteTrigger(_id); + await deleteTrigger(); dispatchToastMessage({ type: 'success', message: t('Trigger_removed') }); reload(); } catch (error) { diff --git a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.js b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.js deleted file mode 100644 index 03de7930f3ff..000000000000 --- a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.js +++ /dev/null @@ -1,170 +0,0 @@ -import { Box, FieldGroup, Field, TextInput, MultiSelect, Button, ButtonGroup, NumberInput } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { ExternalLink } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; - -import Page from '../../../components/Page'; -import { useForm } from '../../../hooks/useForm'; - -const reduceSendOptions = (options) => - Object.entries(options).reduce((acc, [key, val]) => { - if (val) { - acc = [...acc, key]; - } - return acc; - }, []); - -const integrationsUrl = 'https://docs.rocket.chat/use-rocket.chat/omnichannel/webhooks'; - -const getInitialValues = ({ - Livechat_webhookUrl, - Livechat_secret_token, - Livechat_webhook_on_start, - Livechat_webhook_on_close, - Livechat_webhook_on_chat_taken, - Livechat_webhook_on_chat_queued, - Livechat_webhook_on_forward, - Livechat_webhook_on_offline_msg, - Livechat_webhook_on_visitor_message, - Livechat_webhook_on_agent_message, - Livechat_http_timeout, -}) => { - const sendOptions = { - Livechat_webhook_on_start, - Livechat_webhook_on_close, - Livechat_webhook_on_chat_taken, - Livechat_webhook_on_chat_queued, - Livechat_webhook_on_forward, - Livechat_webhook_on_offline_msg, - Livechat_webhook_on_visitor_message, - Livechat_webhook_on_agent_message, - }; - - const mappedSendOptions = reduceSendOptions(sendOptions); - - return { - Livechat_webhookUrl, - Livechat_secret_token, - Livechat_http_timeout, - sendOn: mappedSendOptions, - }; -}; - -const WebhooksPage = ({ settings }) => { - const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - - const { values, handlers, hasUnsavedChanges, reset, commit } = useForm(getInitialValues(settings)); - - const save = useEndpoint('POST', '/v1/omnichannel/integrations'); - const test = useEndpoint('POST', '/v1/livechat/webhook.test'); - - const { Livechat_webhookUrl, Livechat_secret_token, Livechat_http_timeout, sendOn } = values; - - const { handleLivechat_webhookUrl, handleLivechat_secret_token, handleLivechat_http_timeout, handleSendOn } = handlers; - - const sendOptions = useMemo( - () => [ - ['Livechat_webhook_on_start', t('Chat_start')], - ['Livechat_webhook_on_close', t('Chat_close')], - ['Livechat_webhook_on_chat_taken', t('Chat_taken')], - ['Livechat_webhook_on_chat_queued', t('Chat_queued')], - ['Livechat_webhook_on_forward', t('Forwarding')], - ['Livechat_webhook_on_offline_msg', t('Offline_messages')], - ['Livechat_webhook_on_visitor_message', t('Visitor_message')], - ['Livechat_webhook_on_agent_message', t('Agent_messages')], - ], - [t], - ); - - const handleSave = useMutableCallback(async () => { - try { - await save({ - LivechatWebhookUrl: Livechat_webhookUrl, - LivechatSecretToken: Livechat_secret_token, - LivechatHttpTimeout: Livechat_http_timeout, - LivechatWebhookOnStart: sendOn.includes('Livechat_webhook_on_start'), - LivechatWebhookOnClose: sendOn.includes('Livechat_webhook_on_close'), - LivechatWebhookOnChatTaken: sendOn.includes('Livechat_webhook_on_chat_taken'), - LivechatWebhookOnChatQueued: sendOn.includes('Livechat_webhook_on_chat_queued'), - LivechatWebhookOnForward: sendOn.includes('Livechat_webhook_on_forward'), - LivechatWebhookOnOfflineMsg: sendOn.includes('Livechat_webhook_on_offline_msg'), - LivechatWebhookOnVisitorMessage: sendOn.includes('Livechat_webhook_on_visitor_message'), - LivechatWebhookOnAgentMessage: sendOn.includes('Livechat_webhook_on_agent_message'), - }); - dispatchToastMessage({ type: 'success', message: t('Saved') }); - commit(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - - const handleTest = useMutableCallback(async () => { - try { - await test(); - dispatchToastMessage({ type: 'success', message: t('It_works') }); - commit(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - - return ( - - - - - - - - - - -

{t('You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM')}

-

- {t('Click_here')} {t('to_see_more_details_on_how_to_integrate')} -

- - - {t('Webhook_URL')} - - - - - - {t('Secret_token')} - - - - - - {t('Send_request_on')} - - - - - - - - {t('Http_timeout')} - - - - - -
-
-
- ); -}; - -export default WebhooksPage; diff --git a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx new file mode 100644 index 000000000000..437c4a9a2afc --- /dev/null +++ b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx @@ -0,0 +1,233 @@ +import type { SettingValue } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Box, + FieldGroup, + Field, + FieldRow, + TextInput, + MultiSelect, + Button, + ButtonGroup, + NumberInput, + FieldLabel, +} from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { ExternalLink } from '@rocket.chat/ui-client'; +import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React, { useMemo } from 'react'; +import { Controller, useForm, useWatch } from 'react-hook-form'; + +import Page from '../../../components/Page'; + +type WebhooksPageProps = { + settings: Record; +}; + +type SendOnOptions = + | 'Livechat_webhook_on_start' + | 'Livechat_webhook_on_close' + | 'Livechat_webhook_on_chat_taken' + | 'Livechat_webhook_on_chat_queued' + | 'Livechat_webhook_on_forward' + | 'Livechat_webhook_on_offline_msg' + | 'Livechat_webhook_on_visitor_message' + | 'Livechat_webhook_on_agent_message'; + +type WebhookFormValues = { + Livechat_webhookUrl: string | undefined; + Livechat_secret_token: string | undefined; + Livechat_http_timeout: string | undefined; + sendOn: SendOnOptions[]; +}; + +const reduceSendOptions = (options: Record) => + Object.entries(options).reduce((acc, [key, val]) => { + if (val) { + acc = [...acc, key]; + } + return acc; + }, []); + +const INTEGRATION_URL = 'https://docs.rocket.chat/use-rocket.chat/omnichannel/webhooks'; + +const getInitialValues = ({ + Livechat_webhookUrl, + Livechat_secret_token, + Livechat_webhook_on_start, + Livechat_webhook_on_close, + Livechat_webhook_on_chat_taken, + Livechat_webhook_on_chat_queued, + Livechat_webhook_on_forward, + Livechat_webhook_on_offline_msg, + Livechat_webhook_on_visitor_message, + Livechat_webhook_on_agent_message, + Livechat_http_timeout, +}: WebhooksPageProps['settings']): WebhookFormValues => { + const mappedSendOptions = reduceSendOptions({ + Livechat_webhook_on_start, + Livechat_webhook_on_close, + Livechat_webhook_on_chat_taken, + Livechat_webhook_on_chat_queued, + Livechat_webhook_on_forward, + Livechat_webhook_on_offline_msg, + Livechat_webhook_on_visitor_message, + Livechat_webhook_on_agent_message, + }); + + return { + Livechat_webhookUrl, + Livechat_secret_token, + Livechat_http_timeout, + sendOn: mappedSendOptions, + } as WebhookFormValues; +}; + +const WebhooksPage = ({ settings }: WebhooksPageProps) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const defaultValues = getInitialValues(settings); + const { + control, + reset, + formState: { isDirty, isSubmitting }, + handleSubmit, + } = useForm({ + defaultValues, + }); + + const save = useEndpoint('POST', '/v1/omnichannel/integrations'); + const test = useEndpoint('POST', '/v1/livechat/webhook.test'); + + const livechatWebhookUrl = useWatch({ name: 'Livechat_webhookUrl', control }); + const canTest = !(livechatWebhookUrl && !isDirty); + + const sendOptions = useMemo( + () => [ + ['Livechat_webhook_on_start', t('Chat_start')], + ['Livechat_webhook_on_close', t('Chat_close')], + ['Livechat_webhook_on_chat_taken', t('Chat_taken')], + ['Livechat_webhook_on_chat_queued', t('Chat_queued')], + ['Livechat_webhook_on_forward', t('Forwarding')], + ['Livechat_webhook_on_offline_msg', t('Offline_messages')], + ['Livechat_webhook_on_visitor_message', t('Visitor_message')], + ['Livechat_webhook_on_agent_message', t('Agent_messages')], + ], + [t], + ); + + const handleSave = useMutableCallback(async (values) => { + const { sendOn, Livechat_webhookUrl, Livechat_secret_token, Livechat_http_timeout } = values; + try { + await save({ + LivechatWebhookUrl: Livechat_webhookUrl, + LivechatSecretToken: Livechat_secret_token, + LivechatHttpTimeout: Livechat_http_timeout, + LivechatWebhookOnStart: sendOn.includes('Livechat_webhook_on_start'), + LivechatWebhookOnClose: sendOn.includes('Livechat_webhook_on_close'), + LivechatWebhookOnChatTaken: sendOn.includes('Livechat_webhook_on_chat_taken'), + LivechatWebhookOnChatQueued: sendOn.includes('Livechat_webhook_on_chat_queued'), + LivechatWebhookOnForward: sendOn.includes('Livechat_webhook_on_forward'), + LivechatWebhookOnOfflineMsg: sendOn.includes('Livechat_webhook_on_offline_msg'), + LivechatWebhookOnVisitorMessage: sendOn.includes('Livechat_webhook_on_visitor_message'), + LivechatWebhookOnAgentMessage: sendOn.includes('Livechat_webhook_on_agent_message'), + }); + + reset(values); + dispatchToastMessage({ type: 'success', message: t('Saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const testWebhook = useMutation({ + mutationFn: () => test(), + onSuccess: () => dispatchToastMessage({ type: 'success', message: t('It_works') }), + onError: (error) => dispatchToastMessage({ type: 'error', message: error }), + }); + + return ( + + + + + + + + + + +

{t('You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM')}

+

+ {t('Click_here')} {t('to_see_more_details_on_how_to_integrate')} +

+ + + {t('Webhook_URL')} + + ( + + )} + /> + + + + {t('Secret_token')} + + ( + + )} + /> + + + + {t('Send_request_on')} + + + ( + + )} + /> + + + + + {t('Http_timeout')} + + ( + + )} + /> + + + +
+
+
+ ); +}; + +export default WebhooksPage; diff --git a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.js b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx similarity index 51% rename from apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.js rename to apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx index d1e7379e1c3c..5442f3d27c90 100644 --- a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.js +++ b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx @@ -1,16 +1,16 @@ +import type { ISetting, Serialized, SettingValue } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; -import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import React from 'react'; import Page from '../../../components/Page'; import PageSkeleton from '../../../components/PageSkeleton'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import WebhooksPage from './WebhooksPage'; -const reduceSettings = (settings) => - settings.reduce((acc, { _id, value }) => { +const reduceSettings = (settings: Serialized[]) => + settings.reduce>((acc, { _id, value }) => { acc = { ...acc, [_id]: value }; return acc; }, {}); @@ -18,7 +18,12 @@ const reduceSettings = (settings) => const WebhooksPageContainer = () => { const t = useTranslation(); - const { value: data, phase: state, error } = useEndpointData('/v1/livechat/integrations.settings'); + const getIntegrationsSettings = useEndpoint('GET', '/v1/livechat/integrations.settings'); + + const { data, isLoading, isError } = useQuery(['/v1/livechat/integrations.settings'], async () => { + const { settings, success } = await getIntegrationsSettings(); + return { settings: reduceSettings(settings), success }; + }); const canViewLivechatWebhooks = usePermission('view-livechat-webhooks'); @@ -26,11 +31,11 @@ const WebhooksPageContainer = () => { return ; } - if (state === AsyncStatePhase.LOADING) { + if (isLoading) { return ; } - if (!data || !data.success || !data.settings || error) { + if (!data?.success || !data?.settings || isError) { return ( @@ -41,7 +46,7 @@ const WebhooksPageContainer = () => { ); } - return ; + return ; }; export default WebhooksPageContainer; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7f376341992d..54ce71bd80ec 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -300,8 +300,9 @@ export const useQuickActions = (): { const manualOnHoldAllowed = useSetting('Livechat_allow_manual_on_hold'); const hasManagerRole = useRole('livechat-manager'); + const hasMonitorRole = useRole('livechat-monitor'); - const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole) && room?.lastMessage?.t !== 'livechat-close'; + const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole || hasMonitorRole) && room?.lastMessage?.t !== 'livechat-close'; const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined; const canForwardGuest = usePermission('transfer-livechat-guest'); const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index de9a96dc43b4..da598c00be11 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -141,7 +141,7 @@ const MessageBox = ({ [chat, storageID], ); - const autofocusRef = useMessageBoxAutoFocus(); + const autofocusRef = useMessageBoxAutoFocus(!isMobile); const useEmojis = useUserPreference('useEmojis'); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx index 3a1f1eef6dcc..419c6c2cfdda 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx @@ -14,8 +14,8 @@ const CreateDiscussionAction = ({ room }: { room: IRoom }) => { setModal( setModal(null)} defaultParentRoom={room?.prid || room?._id} />); const discussionEnabled = useSetting('Discussion_enabled') as boolean; - const canStartDiscussion = usePermission('start-discussion'); - const canSstartDiscussionOtherUser = usePermission('start-discussion-other-user'); + const canStartDiscussion = usePermission('start-discussion', room._id); + const canSstartDiscussionOtherUser = usePermission('start-discussion-other-user', room._id); const allowDiscussion = room && discussionEnabled && !isRoomFederated(room) && (canStartDiscussion || canSstartDiscussionOtherUser); diff --git a/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts b/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts index 5ea6db79a869..b8efd9391f87 100644 --- a/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts +++ b/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts @@ -1,13 +1,13 @@ import type { Ref } from 'react'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; /** * if the user is types outside the message box and its not actually typing in any input field * then the message box should be focused * @returns callbackRef to bind the logic to the message box */ -export const useMessageBoxAutoFocus = (): Ref => { - const ref = useRef(null); +export const useMessageBoxAutoFocus = (enabled: boolean): Ref => { + const ref = useRef(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -43,5 +43,22 @@ export const useMessageBoxAutoFocus = (): Ref => { }; }, []); - return ref; + return useCallback( + (node: HTMLElement | null) => { + if (!node) { + return; + } + + ref.current = node; + + if (!enabled) { + return; + } + + if (ref.current) { + ref.current.focus(); + } + }, + [enabled, ref], + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index a15edebf8c16..6952b5b1dafe 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -1,4 +1,4 @@ -import { FieldGroup, Field, ToggleSwitch, Select } from '@rocket.chat/fuselage'; +import { FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; @@ -41,14 +41,14 @@ const AutoTranslate = ({ - + - {t('Automatic_Translation')} - + {t('Automatic_Translation')} + - {t('Language')} - + {t('Language')} + setType(String(value))} placeholder={t('Type')} options={exportOptions} /> - + {type && type === 'file' && } diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx index efbf60b69606..2085e11ea11d 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, Select, ButtonGroup, Button, FieldGroup, InputBox } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, Select, ButtonGroup, Button, FieldGroup, InputBox } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, MouseEventHandler } from 'react'; import React, { useMemo } from 'react'; @@ -65,22 +65,22 @@ const FileExport: FC = ({ onCancel, rid }) => { return ( - {t('Date_From')} - + {t('Date_From')} + - + - {t('Date_to')} - + {t('Date_to')} + - + - {t('Output_format')} - + {t('Output_format')} + {children} - + ); diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx index 9f64bbc6504b..709699ebd697 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx @@ -1,4 +1,4 @@ -import { Field, ButtonGroup, Button, CheckBox, Callout } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, ButtonGroup, Button, CheckBox, Callout } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -45,7 +45,7 @@ const PruneMessages = ({ callOutText, validateText, onClickClose, onClickPrune } - {t('Only_from_users')} + {t('Only_from_users')} - + - {t('Inclusive')} - + {t('Inclusive')} + - + - {t('RetentionPolicy_DoNotPrunePinned')} - + {t('RetentionPolicy_DoNotPrunePinned')} + - + - {t('RetentionPolicy_DoNotPruneDiscussion')} - + {t('RetentionPolicy_DoNotPruneDiscussion')} + - + - {t('RetentionPolicy_DoNotPruneThreads')} - + {t('RetentionPolicy_DoNotPruneThreads')} + - + - {t('Files_only')} - + {t('Files_only')} + {callOutText && !validateText && {callOutText}} {validateText && {validateText}} diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx index e774311e5564..38bf1c233f5a 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesDateTimeRow.tsx @@ -1,4 +1,4 @@ -import { Field, InputBox, Box, Margins } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, InputBox, Box, Margins } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; @@ -13,7 +13,7 @@ const PruneMessagesDateTimeRow = ({ label, field }: PruneMessagesDateTimeRowProp return ( - {label} + {label} diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 7a8f0d1e699a..09eaca08cbc9 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Field, Button, ButtonGroup, FieldGroup } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, Button, ButtonGroup, FieldGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -65,7 +65,7 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => - {t('Choose_users')} + {t('Choose_users')} {isRoomFederated(room) ? ( - {t('Expiration_(Days)')} - + {t('Expiration_(Days)')} + )} /> - + - {t('Max_number_of_uses')} - + {t('Max_number_of_uses')} + )} /> - +
- ); -} diff --git a/packages/livechat/src/components/FilesDropTarget/index.tsx b/packages/livechat/src/components/FilesDropTarget/index.tsx new file mode 100644 index 000000000000..3e9935c37565 --- /dev/null +++ b/packages/livechat/src/components/FilesDropTarget/index.tsx @@ -0,0 +1,122 @@ +import type { ComponentChildren, Ref } from 'preact'; +import type { TargetedEvent } from 'preact/compat'; +import { useState } from 'preact/hooks'; +import type { JSXInternal } from 'preact/src/jsx'; + +import { createClassName } from '../../helpers/createClassName'; +import styles from './styles.scss'; + +const escapeForRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +type FilesDropTargetProps = { + overlayed?: boolean; + overlayText?: string; + accept?: string; + multiple?: boolean; + className?: string; + style?: JSXInternal.CSSProperties; + children?: ComponentChildren; + inputRef?: Ref; + onUpload?: (files: File[]) => void; +}; + +export const FilesDropTarget = ({ + overlayed, + overlayText, + accept, + multiple, + className, + style = {}, + children, + inputRef, + onUpload, +}: FilesDropTargetProps) => { + const [dragLevel, setDragLevel] = useState(0); + + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + }; + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setDragLevel(dragLevel + 1); + }; + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault(); + setDragLevel(dragLevel - 1); + }; + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + + if (dragLevel === 0 || !event?.dataTransfer?.files?.length) { + return; + } + + setDragLevel(0); + + handleUpload(event?.dataTransfer?.files); + }; + + const handleInputChange = (event: TargetedEvent) => { + if (!event?.currentTarget?.files?.length) { + return; + } + + handleUpload(event.currentTarget.files); + }; + + const handleUpload = (files: FileList) => { + if (!onUpload) { + return; + } + + let filteredFiles = Array.from(files); + + if (accept) { + const acceptMatchers = accept.split(',').map((acceptString) => { + if (acceptString.charAt(0) === '.') { + return ({ name }: { name: string }) => new RegExp(`${escapeForRegExp(acceptString)}$`, 'i').test(name); + } + + const matchTypeOnly = /^(.+)\/\*$/i.exec(acceptString); + if (matchTypeOnly) { + return ({ type }: { type: string }) => new RegExp(`^${escapeForRegExp(matchTypeOnly[1])}/.*$`, 'i').test(type); + } + + return ({ type }: { type: string }) => new RegExp(`^s${escapeForRegExp(acceptString)}$`, 'i').test(type); + }); + + filteredFiles = filteredFiles.filter((file) => acceptMatchers.some((acceptMatcher) => acceptMatcher(file))); + } + + if (!multiple) { + filteredFiles = filteredFiles.slice(0, 1); + } + + filteredFiles.length && onUpload(filteredFiles); + }; + + return ( +
0 }, [className])} + style={style} + > + + {children} +
+ ); +}; diff --git a/packages/livechat/src/components/FilesDropTarget/stories.tsx b/packages/livechat/src/components/FilesDropTarget/stories.tsx index e1a01cbaea09..d687fbaad492 100644 --- a/packages/livechat/src/components/FilesDropTarget/stories.tsx +++ b/packages/livechat/src/components/FilesDropTarget/stories.tsx @@ -71,7 +71,7 @@ AcceptingMultipleFiles.args = { export const TriggeringBrowseAction = Template.bind({}); TriggeringBrowseAction.storyName = 'triggering browse action'; -const ref = createRef(); +const inputRef = createRef(); TriggeringBrowseAction.args = { children: (
- +
), - ref, + inputRef, }; diff --git a/packages/livechat/src/components/Header/index.js b/packages/livechat/src/components/Header/index.tsx similarity index 67% rename from packages/livechat/src/components/Header/index.js rename to packages/livechat/src/components/Header/index.tsx index 669b46571b0e..9764c3c2ce37 100644 --- a/packages/livechat/src/components/Header/index.js +++ b/packages/livechat/src/components/Header/index.tsx @@ -1,12 +1,41 @@ +import type { ComponentChildren, Ref } from 'preact'; import { toChildArray } from 'preact'; +import type { JSXInternal } from 'preact/src/jsx'; import { createClassName } from '../../helpers/createClassName'; import styles from './styles.scss'; -export const Header = ({ children, theme: { color: backgroundColor, fontColor: color } = {}, className, post, large, style, ...props }) => ( +type HeaderProps = { + children?: ComponentChildren; + theme?: { + color?: string; + fontColor?: string; + }; + className?: string; + post?: ComponentChildren; + large?: boolean; + style?: JSXInternal.CSSProperties; + ref?: Ref; + onClick?: JSXInternal.DOMAttributes['onClick']; +}; + +type HeaderComponentProps = { + children?: ComponentChildren; + className?: string; +}; + +export const Header = ({ + children, + theme: { color: backgroundColor, fontColor: color } = {}, + className, + post, + large, + style, + ...props +}: HeaderProps) => (
{children} @@ -14,25 +43,25 @@ export const Header = ({ children, theme: { color: backgroundColor, fontColor: c
); -export const Picture = ({ children, className = undefined, ...props }) => ( +export const Picture = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const Content = ({ children, className = undefined, ...props }) => ( +export const Content = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const Title = ({ children, className = undefined, ...props }) => ( +export const Title = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const SubTitle = ({ children, className = undefined, ...props }) => ( +export const SubTitle = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
(
); -export const Actions = ({ children, className = undefined, ...props }) => ( +export const Actions = ({ children, className = undefined, ...props }: HeaderComponentProps) => ( ); -export const Action = ({ children, className = undefined, ...props }) => ( +export const Action = ({ children, className = undefined, ...props }: HeaderComponentProps & { onClick?: () => void }) => ( ); -export const Post = ({ children, className = undefined, ...props }) => ( +export const Post = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const CustomField = ({ children, className = undefined, ...props }) => ( +export const CustomField = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
diff --git a/packages/livechat/src/components/Modal/styles.scss b/packages/livechat/src/components/Modal/styles.scss index 6c3a7fd38957..359a953bf124 100644 --- a/packages/livechat/src/components/Modal/styles.scss +++ b/packages/livechat/src/components/Modal/styles.scss @@ -59,6 +59,12 @@ $modal-background-color: $bg-color-white; line-height: 1.5; } +@media (prefers-reduced-motion) { + .modal--animated { + animation: none; + } +} + @keyframes fadeInUp { 0% { transform: translate3d(-50%, 100%, 0); diff --git a/packages/livechat/src/components/Screen/Header.js b/packages/livechat/src/components/Screen/Header.tsx similarity index 54% rename from packages/livechat/src/components/Screen/Header.js rename to packages/livechat/src/components/Screen/Header.tsx index 9f9e41810c7b..671a5b343b21 100644 --- a/packages/livechat/src/components/Screen/Header.js +++ b/packages/livechat/src/components/Screen/Header.tsx @@ -1,6 +1,8 @@ -import { Component } from 'preact'; -import { withTranslation } from 'react-i18next'; +import type { ComponentChildren } from 'preact'; +import { useRef } from 'preact/hooks'; +import { useTranslation, withTranslation } from 'react-i18next'; +import type { Agent } from '../../definitions/agents'; import MinimizeIcon from '../../icons/arrowDown.svg'; import RestoreIcon from '../../icons/arrowUp.svg'; import NotificationsEnabledIcon from '../../icons/bell.svg'; @@ -11,70 +13,84 @@ import { Avatar } from '../Avatar'; import Header from '../Header'; import Tooltip from '../Tooltip'; -class ScreenHeader extends Component { - largeHeader = () => { - const { agent } = this.props; - return !!(agent && agent.email && agent.phone); +type screenHeaderProps = { + alerts: { id: string; children: ComponentChildren; [key: string]: unknown }[]; + agent: Agent; + notificationsEnabled: boolean; + minimized: boolean; + expanded: boolean; + windowed: boolean; + onDismissAlert?: (id?: string) => void; + onEnableNotifications: () => unknown; + onDisableNotifications: () => unknown; + onMinimize: () => unknown; + onRestore: () => unknown; + onOpenWindow: () => unknown; + queueInfo: { + spot: number; }; + title: string; +}; - headerTitle = (t) => { - const { agent, queueInfo, title } = this.props; - if (agent && agent.name) { +const ScreenHeader = ({ + alerts, + agent, + notificationsEnabled, + minimized, + expanded, + windowed, + onDismissAlert, + onEnableNotifications, + onDisableNotifications, + onMinimize, + onRestore, + onOpenWindow, + queueInfo, + title, +}: screenHeaderProps) => { + const { t } = useTranslation(); + const headerRef = useRef(null); + + const largeHeader = () => { + return !!(agent?.email && agent.phone); + }; + + const headerTitle = () => { + if (agent?.name) { return agent.name; } - if (queueInfo && queueInfo.spot && queueInfo.spot > 0) { + if (queueInfo?.spot && queueInfo.spot > 0) { return t('waiting_queue'); } return title; }; - render = ({ - alerts, - agent, - notificationsEnabled, - minimized, - expanded, - windowed, - onDismissAlert, - onEnableNotifications, - onDisableNotifications, - onMinimize, - onRestore, - onOpenWindow, - t, - }) => ( + return (
- {alerts && - alerts.map((alert) => ( - - {alert.children} - - ))} + {alerts?.map((alert) => ( + + {alert.children} + + ))} } - large={this.largeHeader()} + large={largeHeader()} > - {agent && agent.avatar && ( + {agent?.avatar && ( - + )} - {this.headerTitle(t)} - {agent && agent.email && {agent.email}} - {agent && agent.phone && {agent.phone}} + {headerTitle()} + {agent?.email && {agent.email}} + {agent?.phone && {agent.phone}} @@ -108,6 +124,6 @@ class ScreenHeader extends Component {
); -} +}; export default withTranslation()(ScreenHeader); diff --git a/packages/livechat/src/components/Tooltip/index.js b/packages/livechat/src/components/Tooltip/index.js index 2f5368d8729c..3b2d36f3609a 100644 --- a/packages/livechat/src/components/Tooltip/index.js +++ b/packages/livechat/src/components/Tooltip/index.js @@ -98,7 +98,7 @@ export class TooltipContainer extends Component { } } -export const TooltipTrigger = ({ children, content, placement }) => ( +export const TooltipTrigger = ({ children, content, placement = '' }) => ( {({ showTooltip, hideTooltip }) => toChildArray(children).map((child, index) => diff --git a/packages/livechat/src/definitions/agents.d.ts b/packages/livechat/src/definitions/agents.d.ts new file mode 100644 index 000000000000..da1b81242574 --- /dev/null +++ b/packages/livechat/src/definitions/agents.d.ts @@ -0,0 +1,13 @@ +// TODO: Fully type agents in livechat +export type Agent = { + name?: string; + status?: string; + email?: string; + phone?: string; + username: string; + avatar?: { + description: string; + src: string; + }; + [key: string]: unknown; +}; diff --git a/packages/livechat/src/lib/api.js b/packages/livechat/src/lib/api.js index 7f4c5ca340a4..ac7df77072b4 100644 --- a/packages/livechat/src/lib/api.js +++ b/packages/livechat/src/lib/api.js @@ -8,9 +8,9 @@ export const normalizeQueueAlert = async (queueInfo) => { if (!queueInfo) { return; } - const formatDistance = await import('date-fns/formatDistance'); + const { default: formatDistance } = await import('date-fns/formatDistance'); const { spot, estimatedWaitTimeSeconds } = queueInfo; - const locale = getDateFnsLocale(); + const locale = await getDateFnsLocale(); const estimatedWaitTime = estimatedWaitTimeSeconds && formatDistance(new Date().setSeconds(estimatedWaitTimeSeconds), new Date(), { locale }); return ( diff --git a/packages/livechat/src/lib/random.ts b/packages/livechat/src/lib/random.ts index 705fdcacc7da..068c84c1c459 100644 --- a/packages/livechat/src/lib/random.ts +++ b/packages/livechat/src/lib/random.ts @@ -4,6 +4,6 @@ export const chooseElement = Random.choice; export const createRandomString = Random._randomString; -export const createRandomId = Random.id; +export const createRandomId = () => Random.id(); export const createToken = () => Random.hexString(64); diff --git a/packages/livechat/src/routes/Chat/component.js b/packages/livechat/src/routes/Chat/component.js index 98991ab72b53..8bd9ac468c6e 100644 --- a/packages/livechat/src/routes/Chat/component.js +++ b/packages/livechat/src/routes/Chat/component.js @@ -1,4 +1,4 @@ -import { Component } from 'preact'; +import { Component, createRef } from 'preact'; import { Suspense, lazy } from 'preact/compat'; import { withTranslation } from 'react-i18next'; @@ -35,6 +35,8 @@ class Chat extends Component { emojiPickerActive: false, }; + inputRef = createRef(null); + handleFilesDropTargetRef = (ref) => { this.filesDropTarget = ref; }; @@ -61,7 +63,7 @@ class Chat extends Component { handleUploadClick = (event) => { event.preventDefault(); - this.filesDropTarget.browse(); + this.inputRef?.current?.click(); }; handleSendClick = (event) => { @@ -151,7 +153,7 @@ class Chat extends Component { handleEmojiClick={this.handleEmojiClick} {...props} > - + {incomingCallAlert && !!incomingCallAlert.show && } {incomingCallAlert?.show && ongoingCall && ongoingCall.callStatus === CallStatus.IN_PROGRESS_SAME_TAB ? ( diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md new file mode 100644 index 000000000000..915bcabc5395 --- /dev/null +++ b/packages/mock-providers/CHANGELOG.md @@ -0,0 +1,17 @@ +# @rocket.chat/mock-providers + +## 0.0.2 + +### Patch Changes + +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- Updated dependencies [b8f3d5014f] + - @rocket.chat/i18n@0.0.2 + +## 0.0.2-rc.0 + +### Patch Changes + +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- Updated dependencies [b8f3d5014f] + - @rocket.chat/i18n@0.0.2-rc.0 diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index d739e442879e..c2aeb2665350 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.0.1", + "version": "0.0.2", "private": true, "dependencies": { "@rocket.chat/i18n": "workspace:~", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 836eb3a1dcc8..8d03eaafa208 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,117 @@ # @rocket.chat/model-typings +## 0.1.0 + +### Minor Changes + +- 4186eecf05: Introduce the ability to report an user +- ead7c7bef2: Fixed read receipts not getting deleted after corresponding message is deleted + +### Patch Changes + +- 8a59855fcf: When setting a room as read-only, do not allow previously unmuted users to send messages. +- 5cee21468e: Fix spotlight search does not find rooms with special or non-latin characters +- aaefe865a7: fix: agent role being removed upon user deactivation +- f556518fa1: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + +- 61128364d6: Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + +## 0.1.0-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + +## 0.1.0-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 + +## 0.1.0-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 + +## 0.1.0-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 + +## 0.1.0-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 + +## 0.1.0-rc.0 + +### Minor Changes + +- 4186eecf05: Introduce the ability to report an user +- ead7c7bef2: Fixed read receipts not getting deleted after corresponding message is deleted + +### Patch Changes + +- 8a59855fcf: When setting a room as read-only, do not allow previously unmuted users to send messages. +- 5cee21468e: Fix spotlight search does not find rooms with special or non-latin characters +- aaefe865a7: fix: agent role being removed upon user deactivation +- f556518fa1: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + +- 61128364d6: Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + +## 0.0.14 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.8 + +## 0.0.13 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 + +## 0.0.12 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 + +## 0.0.11 + +### Patch Changes + +- 92d25b9c7a: Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + + This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. + + - @rocket.chat/core-typings@6.3.5 + ## 0.0.10 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index bacc3023e5f4..5174bf22f97a 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,13 +1,13 @@ { "name": "@rocket.chat/model-typings", - "version": "0.0.10", + "version": "0.1.0", "private": true, "devDependencies": { "@types/jest": "~29.5.3", "@types/node-rsa": "^1.1.1", "eslint": "~8.45.0", "jest": "~29.6.1", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "ts-jest": "~29.0.5", "typescript": "~5.2.2" }, diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 23e77ff1de29..a1874b144347 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -79,3 +79,4 @@ export * from './models/IAuditLogModel'; export * from './models/ICronHistoryModel'; export * from './models/IMigrationsModel'; export * from './models/IModerationReportsModel'; +export * from './models/ICloudAnnouncementsModel'; diff --git a/packages/model-typings/src/models/IBannersModel.ts b/packages/model-typings/src/models/IBannersModel.ts index 62f33ef5d3b2..4fe496bb954c 100644 --- a/packages/model-typings/src/models/IBannersModel.ts +++ b/packages/model-typings/src/models/IBannersModel.ts @@ -1,10 +1,10 @@ -import type { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; +import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typings'; import type { Document, FindCursor, FindOptions, UpdateResult, InsertOneResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IBannersModel extends IBaseModel { - create(doc: IBanner): Promise>; + create(doc: Optional): Promise>; findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/ICloudAnnouncementsModel.ts b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts new file mode 100644 index 000000000000..672ff8c316a0 --- /dev/null +++ b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts @@ -0,0 +1,6 @@ +import type { Cloud } from '@rocket.chat/core-typings'; + +import type { IBaseModel } from './IBaseModel'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ICloudAnnouncementsModel extends IBaseModel {} diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 68b72be33ba8..20100cbb4f61 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,11 @@ -import type { IMessage, IOmnichannelRoom, IOmnichannelRoomClosingInfo, ISetting, ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { + IMessage, + IOmnichannelRoom, + IOmnichannelRoomClosingInfo, + ISetting, + ILivechatVisitor, + MACStats, +} from '@rocket.chat/core-typings'; import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter } from 'mongodb'; import type { FindPaginated } from '..'; @@ -234,4 +241,7 @@ export interface ILivechatRoomsModel extends IBaseModel { setVisitorInactivityInSecondsById(roomId: string, visitorInactivity: any): Promise; changeVisitorByRoomId(roomId: string, visitor: { _id: string; username: string; token: string }): Promise; unarchiveOneById(roomId: string): Promise; + markVisitorActiveForPeriod(rid: string, period: string): Promise; + getMACStatisticsForPeriod(period: string): Promise; + getMACStatisticsBetweenDates(start: Date, end: Date): Promise; } diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index 370db511dadf..5c598c6a6a97 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -48,4 +48,16 @@ export interface ILivechatVisitorsModel extends IBaseModel { updateById(_id: string, update: UpdateFilter): Promise; saveGuestEmailPhoneById(_id: string, emails: string[], phones: string[]): Promise; + + isVisitorActiveOnPeriod(visitorId: string, period: string): Promise; + + markVisitorActiveForPeriod(visitorId: string, period: string): Promise; + + findOneEnabledById(_id: string, options?: FindOptions): Promise; + + disableById(_id: string): Promise; + + findEnabled(query: Filter, options?: FindOptions): FindCursor; + + countVisitorsOnPeriod(period: string): Promise; } diff --git a/packages/model-typings/src/models/ISettingsModel.ts b/packages/model-typings/src/models/ISettingsModel.ts index 9dc2005867fa..d382d4853a4b 100644 --- a/packages/model-typings/src/models/ISettingsModel.ts +++ b/packages/model-typings/src/models/ISettingsModel.ts @@ -17,6 +17,11 @@ export interface ISettingsModel extends IBaseModel { value: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, ): Promise; + resetValueById( + _id: string, + value?: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, + ): Promise; + incrementValueById(_id: ISetting['_id'], value?: number): Promise; updateOptionsById( diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index aebda87c78cb..53b0a69ec232 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -1,5 +1,15 @@ -import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb'; +import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, AtLeast } from '@rocket.chat/core-typings'; +import type { + FindOptions, + FindCursor, + UpdateResult, + DeleteResult, + Document, + AggregateOptions, + Filter, + InsertOneResult, + InsertManyResult, +} from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -216,6 +226,10 @@ export interface ISubscriptionsModel extends IBaseModel { ): Promise; removeByUserId(userId: string): Promise; createWithRoomAndUser(room: IRoom, user: IUser, extraData?: Record): Promise>; + createWithRoomAndManyUsers( + room: IRoom, + users: { user: AtLeast; extraData: Record }[], + ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; removeByRoomIdAndUserId(roomId: string, userId: string): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index c0ce51f79f45..f14f5bc90d0d 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -239,6 +239,7 @@ export interface IUsersModel extends IBaseModel { removeAllRoomsByUserId(userId: string): Promise; removeRoomByUserId(userId: string, rid: string): Promise; addRoomByUserId(userId: string, rid: string): Promise; + addRoomByUserIds(uids: string[], rid: string): Promise; removeRoomByRoomIds(rids: string[]): Promise; getLoginTokensByUserId(userId: string): FindCursor; addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise; @@ -317,7 +318,7 @@ export interface IUsersModel extends IBaseModel { findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions): FindCursor; findCrowdUsers(options?: FindOptions): FindCursor; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; + findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions): FindCursor; @@ -372,7 +373,7 @@ export interface IUsersModel extends IBaseModel { getUsersToSendOfflineEmail(userIds: string[]): FindCursor>; countActiveUsersByService(service: string, options?: FindOptions): Promise; getActiveLocalUserCount(): Promise; - getActiveLocalGuestCount(): Promise; + getActiveLocalGuestCount(exceptions?: IUser['_id'] | IUser['_id'][]): Promise; removeOlderResumeTokensByUserId(userId: string, fromDate: Date): Promise; findAllUsersWithPendingAvatar(): FindCursor; updateCustomFieldsById(userId: string, customFields: Record): Promise; diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 230aa3158fbd..ac261acbc4d7 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,80 @@ # @rocket.chat/models +## 0.0.15 + +### Patch Changes + +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] + - @rocket.chat/model-typings@0.1.0 + +## 0.0.15-rc.5 + +### Patch Changes + +- @rocket.chat/model-typings@0.1.0-rc.5 + +## 0.0.14-rc.4 + +### Patch Changes + +- @rocket.chat/model-typings@0.1.0-rc.4 + +## 0.0.14-rc.3 + +### Patch Changes + +- @rocket.chat/model-typings@0.1.0-rc.3 + +## 0.0.14-rc.2 + +### Patch Changes + +- @rocket.chat/model-typings@0.1.0-rc.2 + +## 0.0.14-rc.1 + +### Patch Changes + +- @rocket.chat/model-typings@0.1.0-rc.1 + +## 0.0.14-rc.0 + +### Patch Changes + +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] + - @rocket.chat/model-typings@0.1.0-rc.0 + +## 0.0.13 + +### Patch Changes + +- @rocket.chat/model-typings@0.0.13 + +## 0.0.12 + +### Patch Changes + +- @rocket.chat/model-typings@0.0.12 + +## 0.0.11 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + ## 0.0.10 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index c075c8132582..1644a8362c77 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.0.10", + "version": "0.0.15", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index e1cf91f1b0ee..1e83fe72b93e 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -78,6 +78,7 @@ import type { ICronHistoryModel, IMigrationsModel, IModerationReportsModel, + ICloudAnnouncementsModel, } from '@rocket.chat/model-typings'; import { proxify } from './proxify'; @@ -170,3 +171,4 @@ export const AuditLog = proxify('IAuditLogModel'); export const CronHistory = proxify('ICronHistoryModel'); export const Migrations = proxify('IMigrationsModel'); export const ModerationReports = proxify('IModerationReportsModel'); +export const CloudAnnouncements = proxify('ICloudAnnouncementsModel'); diff --git a/packages/password-policies/.eslintrc.json b/packages/password-policies/.eslintrc.json new file mode 100644 index 000000000000..15f2cd4817e1 --- /dev/null +++ b/packages/password-policies/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "plugins": ["jest"], + "env": { + "jest/globals": true + }, + "ignorePatterns": ["**/dist"] +} diff --git a/packages/password-policies/jest.config.ts b/packages/password-policies/jest.config.ts new file mode 100644 index 000000000000..959a31a7c6bf --- /dev/null +++ b/packages/password-policies/jest.config.ts @@ -0,0 +1,3 @@ +export default { + preset: 'ts-jest', +}; diff --git a/packages/password-policies/package.json b/packages/password-policies/package.json new file mode 100644 index 000000000000..52fa766671db --- /dev/null +++ b/packages/password-policies/package.json @@ -0,0 +1,26 @@ +{ + "name": "@rocket.chat/password-policies", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/chai": "^4.3.5", + "@types/jest": "~29.5.3", + "chai": "^4.3.7", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "~29.0.5", + "typescript": "~5.2.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "testunit": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/apps/meteor/app/lib/server/lib/PasswordPolicyClass.js b/packages/password-policies/src/PasswordPolicyClass.ts similarity index 61% rename from apps/meteor/app/lib/server/lib/PasswordPolicyClass.js rename to packages/password-policies/src/PasswordPolicyClass.ts index 99dc0d6fabf5..8212df18002c 100644 --- a/apps/meteor/app/lib/server/lib/PasswordPolicyClass.js +++ b/packages/password-policies/src/PasswordPolicyClass.ts @@ -1,8 +1,45 @@ -import { Random } from '@rocket.chat/random'; -import generator from 'generate-password'; -import { Meteor } from 'meteor/meteor'; +import { PasswordPolicyError } from './PasswordPolicyError'; + +type PasswordPolicyType = { + enabled: boolean; + policy: [name: string, options?: Record][]; +}; + +type ValidationMessageType = { + name: string; + isValid: boolean; + limit?: number; +}; + +export class PasswordPolicy { + private regex: { + forbiddingRepeatingCharacters: RegExp; + mustContainAtLeastOneLowercase: RegExp; + mustContainAtLeastOneUppercase: RegExp; + mustContainAtLeastOneNumber: RegExp; + mustContainAtLeastOneSpecialCharacter: RegExp; + }; + + private enabled: boolean; + + private minLength: number; + + private maxLength: number; + + private forbidRepeatingCharacters: boolean; + + private mustContainAtLeastOneLowercase: boolean; + + private mustContainAtLeastOneUppercase: boolean; + + private mustContainAtLeastOneNumber: boolean; + + private mustContainAtLeastOneSpecialCharacter: boolean; + + private throwError: boolean; + + private forbidRepeatingCharactersCount: number; -class PasswordPolicy { constructor({ enabled = false, minLength = -1, @@ -14,14 +51,7 @@ class PasswordPolicy { mustContainAtLeastOneNumber = false, mustContainAtLeastOneSpecialCharacter = false, throwError = true, - } = {}) { - this.regex = { - mustContainAtLeastOneLowercase: new RegExp('[a-z]'), - mustContainAtLeastOneUppercase: new RegExp('[A-Z]'), - mustContainAtLeastOneNumber: new RegExp('[0-9]'), - mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]'), - }; - + }) { this.enabled = enabled; this.minLength = minLength; this.maxLength = maxLength; @@ -32,27 +62,103 @@ class PasswordPolicy { this.mustContainAtLeastOneNumber = mustContainAtLeastOneNumber; this.mustContainAtLeastOneSpecialCharacter = mustContainAtLeastOneSpecialCharacter; this.throwError = throwError; - } - set forbidRepeatingCharactersCount(value) { - this._forbidRepeatingCharactersCount = value; - this.regex.forbiddingRepeatingCharacters = new RegExp(`(.)\\1{${this.forbidRepeatingCharactersCount},}`); - } - - get forbidRepeatingCharactersCount() { - return this._forbidRepeatingCharactersCount; + this.regex = { + forbiddingRepeatingCharacters: new RegExp(`(.)\\1{${forbidRepeatingCharactersCount},}`), + mustContainAtLeastOneLowercase: new RegExp('[a-z]'), + mustContainAtLeastOneUppercase: new RegExp('[A-Z]'), + mustContainAtLeastOneNumber: new RegExp('[0-9]'), + mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]'), + }; } - error(error, message, reasons) { + error( + error: string, + message: string, + reasons?: { + error: string; + message: string; + }[], + ) { if (this.throwError) { - throw new Meteor.Error(error, message, reasons); + throw new PasswordPolicyError(message, error, reasons); } return false; } - validate(password) { - const reasons = []; + sendValidationMessage(password: string): { + name: string; + isValid: boolean; + limit?: number; + }[] { + const validationReturn: ValidationMessageType[] = []; + + if (!this.enabled) { + return []; + } + + if (this.minLength >= 1) { + validationReturn.push({ + name: 'get-password-policy-minLength', + isValid: !(password.length < this.minLength), + limit: this.minLength, + }); + } + + if (this.maxLength >= 1) { + validationReturn.push({ + name: 'get-password-policy-maxLength', + isValid: !(password.length > this.maxLength), + limit: this.maxLength, + }); + } + + if (this.forbidRepeatingCharacters) { + validationReturn.push({ + name: 'get-password-policy-forbidRepeatingCharactersCount', + isValid: !this.regex.forbiddingRepeatingCharacters.test(password), + limit: this.forbidRepeatingCharactersCount, + }); + } + + if (this.mustContainAtLeastOneLowercase) { + validationReturn.push({ + name: 'get-password-policy-mustContainAtLeastOneLowercase', + isValid: this.regex.mustContainAtLeastOneLowercase.test(password), + }); + } + + if (this.mustContainAtLeastOneUppercase) { + validationReturn.push({ + name: 'get-password-policy-mustContainAtLeastOneUppercase', + isValid: this.regex.mustContainAtLeastOneUppercase.test(password), + }); + } + + if (this.mustContainAtLeastOneNumber) { + validationReturn.push({ + name: 'get-password-policy-mustContainAtLeastOneNumber', + isValid: this.regex.mustContainAtLeastOneNumber.test(password), + }); + } + + if (this.mustContainAtLeastOneSpecialCharacter) { + validationReturn.push({ + name: 'get-password-policy-mustContainAtLeastOneSpecialCharacter', + isValid: this.regex.mustContainAtLeastOneSpecialCharacter.test(password), + }); + } + + return validationReturn; + } + + validate(password: string) { + const reasons: { + error: string; + message: string; + }[] = []; + if (typeof password !== 'string' || !password.trim().length) { return this.error('error-password-policy-not-met', "The password provided does not meet the server's password policy."); } @@ -117,11 +223,12 @@ class PasswordPolicy { return true; } - getPasswordPolicy() { - const data = { + getPasswordPolicy(): PasswordPolicyType { + const data: PasswordPolicyType = { enabled: false, policy: [], }; + if (this.enabled) { data.enabled = true; if (this.minLength >= 1) { @@ -154,31 +261,4 @@ class PasswordPolicy { } return data; } - - generatePassword() { - if (this.enabled) { - for (let i = 0; i < 10; i++) { - const password = this._generatePassword(); - if (this.validate(password)) { - return password; - } - } - } - - return Random.id(); - } - - _generatePassword() { - const length = Math.min(Math.max(this.minLength, 12), this.maxLength > 0 ? this.maxLength : Number.MAX_SAFE_INTEGER); - return generator.generate({ - length, - ...(this.mustContainAtLeastOneNumber && { numbers: true }), - ...(this.mustContainAtLeastOneSpecialCharacter && { symbols: true }), - ...(this.mustContainAtLeastOneLowercase && { lowercase: true }), - ...(this.mustContainAtLeastOneUppercase && { uppercase: true }), - strict: true, - }); - } } - -export default PasswordPolicy; diff --git a/packages/password-policies/src/PasswordPolicyError.ts b/packages/password-policies/src/PasswordPolicyError.ts new file mode 100644 index 000000000000..ea58d0e83b07 --- /dev/null +++ b/packages/password-policies/src/PasswordPolicyError.ts @@ -0,0 +1,11 @@ +export class PasswordPolicyError extends Error { + public error: string; + + public details?: { error: string; message: string }[] | undefined; + + constructor(message: string, error: string, details?: { error: string; message: string }[]) { + super(message); + this.error = error; + this.details = details; + } +} diff --git a/packages/password-policies/src/index.ts b/packages/password-policies/src/index.ts new file mode 100644 index 000000000000..ce94042e029a --- /dev/null +++ b/packages/password-policies/src/index.ts @@ -0,0 +1 @@ +export { PasswordPolicy } from './PasswordPolicyClass'; diff --git a/packages/password-policies/tests/passwordPolicyClass.test.ts b/packages/password-policies/tests/passwordPolicyClass.test.ts new file mode 100644 index 000000000000..4cda16e3dd27 --- /dev/null +++ b/packages/password-policies/tests/passwordPolicyClass.test.ts @@ -0,0 +1,223 @@ +import { expect } from 'chai'; + +import { PasswordPolicy } from '../src/PasswordPolicyClass'; + +describe('PasswordPolicy', () => { + describe('Password tests with default options', () => { + it('should allow all passwords', () => { + const passwordPolicy = new PasswordPolicy({ throwError: false }); + expect(passwordPolicy.validate(null as any)).to.be.equal(false); + expect(passwordPolicy.validate(undefined as any)).to.be.equal(false); + expect(passwordPolicy.validate('')).to.be.equal(false); + expect(passwordPolicy.validate(' ')).to.be.equal(false); + expect(passwordPolicy.validate('a')).to.be.equal(true); + expect(passwordPolicy.validate('aaaaaaaaa')).to.be.equal(true); + }); + }); + + describe('Password tests with options', () => { + it('should not allow non string or empty', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + throwError: false, + }); + expect(passwordPolicy.validate(null as any)).to.be.equal(false); + expect(passwordPolicy.validate(undefined as any)).to.be.false; + expect(passwordPolicy.validate(1 as any)).to.be.false; + expect(passwordPolicy.validate(true as any)).to.be.false; + expect(passwordPolicy.validate(new Date() as any)).to.be.false; + expect(passwordPolicy.validate(new Function() as any)).to.be.false; + expect(passwordPolicy.validate('')).to.be.false; + }); + + it('should restrict by minLength', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + minLength: 5, + throwError: false, + }); + + expect(passwordPolicy.validate('1')).to.be.false; + expect(passwordPolicy.validate('1234')).to.be.false; + expect(passwordPolicy.validate('12345')).to.be.true; + expect(passwordPolicy.validate(' ')).to.be.false; + }); + + it('should restrict by maxLength', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + maxLength: 5, + throwError: false, + }); + + expect(passwordPolicy.validate('1')).to.be.true; + expect(passwordPolicy.validate('12345')).to.be.true; + expect(passwordPolicy.validate('123456')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + }); + + it('should allow repeated characters', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + forbidRepeatingCharacters: false, + throwError: false, + }); + + expect(passwordPolicy.validate('1')).to.be.true; + expect(passwordPolicy.validate('12345')).to.be.true; + expect(passwordPolicy.validate('123456')).to.be.true; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('11111111111111')).to.be.true; + }); + + it('should restrict repeated characters', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + forbidRepeatingCharacters: true, + forbidRepeatingCharactersCount: 3, + throwError: false, + }); + + expect(passwordPolicy.validate('1')).to.be.true; + expect(passwordPolicy.validate('11')).to.be.true; + expect(passwordPolicy.validate('111')).to.be.true; + expect(passwordPolicy.validate('1111')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.true; + }); + + it('should restrict repeated characters customized', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + forbidRepeatingCharacters: true, + forbidRepeatingCharactersCount: 5, + throwError: false, + }); + + expect(passwordPolicy.validate('1')).to.be.true; + expect(passwordPolicy.validate('11')).to.be.true; + expect(passwordPolicy.validate('111')).to.be.true; + expect(passwordPolicy.validate('1111')).to.be.true; + expect(passwordPolicy.validate('11111')).to.be.true; + expect(passwordPolicy.validate('111111')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.true; + }); + + it('should contain one lowercase', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + mustContainAtLeastOneLowercase: true, + throwError: false, + }); + + expect(passwordPolicy.validate('a')).to.be.true; + expect(passwordPolicy.validate('aa')).to.be.true; + expect(passwordPolicy.validate('A')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.false; + expect(passwordPolicy.validate('AAAAA')).to.be.false; + expect(passwordPolicy.validate('AAAaAAA')).to.be.true; + }); + + it('should contain one uppercase', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + mustContainAtLeastOneUppercase: true, + throwError: false, + }); + + expect(passwordPolicy.validate('a')).to.be.false; + expect(passwordPolicy.validate('aa')).to.be.false; + expect(passwordPolicy.validate('A')).to.be.true; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.false; + expect(passwordPolicy.validate('AAAAA')).to.be.true; + expect(passwordPolicy.validate('AAAaAAA')).to.be.true; + }); + + it('should contain one number', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + mustContainAtLeastOneNumber: true, + throwError: false, + }); + + expect(passwordPolicy.validate('a')).to.be.false; + expect(passwordPolicy.validate('aa')).to.be.false; + expect(passwordPolicy.validate('A')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.true; + expect(passwordPolicy.validate('AAAAA')).to.be.false; + expect(passwordPolicy.validate('AAAaAAA')).to.be.false; + expect(passwordPolicy.validate('AAAa1AAA')).to.be.true; + }); + + it('should contain one special character', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + mustContainAtLeastOneSpecialCharacter: true, + throwError: false, + }); + + expect(passwordPolicy.validate('a')).to.be.false; + expect(passwordPolicy.validate('aa')).to.be.false; + expect(passwordPolicy.validate('A')).to.be.false; + expect(passwordPolicy.validate(' ')).to.be.false; + expect(passwordPolicy.validate('123456')).to.be.false; + expect(passwordPolicy.validate('AAAAA')).to.be.false; + expect(passwordPolicy.validate('AAAaAAA')).to.be.false; + expect(passwordPolicy.validate('AAAa1AAA')).to.be.false; + expect(passwordPolicy.validate('AAAa@AAA')).to.be.true; + }); + }); + + describe('Password Policy', () => { + it('should return a correct password policy', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + throwError: false, + minLength: 10, + maxLength: 20, + forbidRepeatingCharacters: true, + forbidRepeatingCharactersCount: 4, + mustContainAtLeastOneLowercase: true, + mustContainAtLeastOneUppercase: true, + mustContainAtLeastOneNumber: true, + mustContainAtLeastOneSpecialCharacter: true, + }); + + const policy = passwordPolicy.getPasswordPolicy(); + + expect(policy).to.not.be.undefined; + expect(policy.enabled).to.be.true; + expect(policy.policy.length).to.be.equal(8); + expect(policy.policy[0][0]).to.be.equal('get-password-policy-minLength'); + expect(policy.policy[0][1]?.minLength).to.be.equal(10); + }); + + it('should return correct values if policy is disabled', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: false, + }); + + const policy = passwordPolicy.getPasswordPolicy(); + + expect(policy.enabled).to.be.false; + expect(policy.policy.length).to.be.equal(0); + }); + + it('should return correct values if policy is enabled but no specifiers exists', () => { + const passwordPolicy = new PasswordPolicy({ + enabled: true, + }); + + const policy = passwordPolicy.getPasswordPolicy(); + + expect(policy.enabled).to.be.true; + // even when no policy is specified, forbidRepeatingCharactersCount is still configured + // since its default value is 3 + expect(policy.policy.length).to.be.equal(1); + }); + }); +}); diff --git a/packages/password-policies/tsconfig.json b/packages/password-policies/tsconfig.json new file mode 100644 index 000000000000..f0a66c843c50 --- /dev/null +++ b/packages/password-policies/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "module": "commonjs" + }, + "include": ["./src/**/*"] +} diff --git a/packages/random/src/NodeRandomGenerator.ts b/packages/random/src/NodeRandomGenerator.ts index b9d556c6ac07..8c9f239413ca 100644 --- a/packages/random/src/NodeRandomGenerator.ts +++ b/packages/random/src/NodeRandomGenerator.ts @@ -38,6 +38,16 @@ export class NodeRandomGenerator extends RandomGenerator { return result.substring(0, digits); } + /** + * @name Random.between Returns a random integer between min and max, inclusive. + * @param min Minimum value (inclusive) + * @param max Maximum value (inclusive) + * @returns A random integer between min and max, inclusive. + */ + between(min: number, max: number) { + return Math.floor(this.fraction() * (max - min + 1)) + min; + } + protected safelyCreateWithSeeds(...seeds: readonly unknown[]) { return new AleaRandomGenerator({ seeds }); } diff --git a/packages/release-action/CHANGELOG.md b/packages/release-action/CHANGELOG.md index ee73fe152636..3435ec55ffa5 100644 --- a/packages/release-action/CHANGELOG.md +++ b/packages/release-action/CHANGELOG.md @@ -1,5 +1,27 @@ # @rocket.chat/release-action +## 2.2.0 + +### Minor Changes + +- f93648a5df: Add back "Engine Versions" to the release notes + +### Patch Changes + +- Updated dependencies [0f56aacc4d] + - @rocket.chat/eslint-config@0.6.0 + +## 2.2.0-rc.0 + +### Minor Changes + +- f93648a5df: Add back "Engine Versions" to the release notes + +### Patch Changes + +- Updated dependencies [0f56aacc4d] + - @rocket.chat/eslint-config@0.6.0-rc.0 + ## 2.1.0 ### Minor Changes diff --git a/packages/release-action/package.json b/packages/release-action/package.json index 769795a56240..0a1e3ded8927 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/release-action", - "version": "2.1.0", + "version": "2.2.0", "private": true, "scripts": { "build": "tsc", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 62f05e378e4e..da3d48464546 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,116 @@ # @rocket.chat/rest-typings +## 6.4.0 + +### Minor Changes + +- 239a34e877: new: ring mobile users on direct conference calls +- 4186eecf05: Introduce the ability to report an user +- 2db32f0d4a: Add option to select what URL previews should be generated for each message. +- 19aec23cda: New AddUser workflow for Federated Rooms +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- 357a3a50fa: feat: high-contrast theme +- 1041d4d361: Added option to select between two script engine options for the integrations +- 93d4912e17: fix: missing params on updateOwnBasicInfo endpoint + +### Patch Changes + +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- 9496f1eb97: Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + +## 6.4.0-rc.5 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + +## 6.4.0-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 + +## 6.4.0-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 + +## 6.4.0-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 + +## 6.4.0-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 + +## 6.4.0-rc.0 + +### Minor Changes + +- 239a34e877: new: ring mobile users on direct conference calls +- 4186eecf05: Introduce the ability to report an user +- 2db32f0d4a: Add option to select what URL previews should be generated for each message. +- 19aec23cda: New AddUser workflow for Federated Rooms +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- 357a3a50fa: feat: high-contrast theme +- 93d4912e17: fix: missing params on updateOwnBasicInfo endpoint + +### Patch Changes + +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- 9496f1eb97: Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + +## 6.3.8 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.8 + +## 6.3.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 + +## 6.3.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 + +## 6.3.5 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.5 + ## 6.3.4 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 45bb773b7dee..d5996987acf1 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,13 +1,13 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.3.4", + "version": "6.4.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/jest": "~29.5.3", "eslint": "~8.45.0", "jest": "~29.6.1", "jest-environment-jsdom": "~29.6.1", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "ts-jest": "~29.0.5", "typescript": "~5.2.2" }, @@ -26,8 +26,9 @@ "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/license": "workspace:^", "@rocket.chat/message-parser": "next", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "ajv": "^8.11.0", "ajv-formats": "^2.1.1" }, diff --git a/packages/rest-typings/src/default/index.ts b/packages/rest-typings/src/default/index.ts index b3aa5d3aa535..0be60fc4413b 100644 --- a/packages/rest-typings/src/default/index.ts +++ b/packages/rest-typings/src/default/index.ts @@ -1,36 +1,38 @@ // eslint-disable-next-line @typescript-eslint/naming-convention export interface DefaultEndpoints { '/info': { - GET: () => - | { - info: { - build: { - arch: string; - cpus: number; - date: string; - freeMemory: number; - nodeVersion: string; - osRelease: string; - platform: string; - totalMemory: number; - }; - commit: { - author?: string; - branch?: string; - date?: string; - hash?: string; - subject?: string; - tag?: string; - }; - marketplaceApiVersion: string; - version: string; - tag?: string; - branch?: string; - }; - } - | { - version: string | undefined; - }; + GET: () => { + info: { + build: { + arch: string; + cpus: number; + date: string; + freeMemory: number; + nodeVersion: string; + osRelease: string; + platform: string; + totalMemory: number; + }; + commit: { + author?: string; + branch?: string; + date?: string; + hash?: string; + subject?: string; + tag?: string; + }; + marketplaceApiVersion: string; + version: string; + tag?: string; + branch?: string; + }; + supportedVersions?: { signed: string }; + minimumClientVersions: { + desktop: string; + mobile: string; + }; + version: string | undefined; + }; }; '/ecdh_proxy/initEncryptedSession': { POST: () => void; diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 066e3248dc33..3b8197ce20bf 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -228,6 +228,7 @@ export * from './v1/invites'; export * from './v1/dm'; export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; +export * from './v1/licenses'; export * from './v1/omnichannel'; export * from './v1/oauthapps'; export * from './v1/oauthapps/UpdateOAuthAppParamsPOST'; diff --git a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts index 3690d0672ce2..914739d000a0 100644 --- a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts +++ b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts @@ -21,7 +21,7 @@ const AutotranslateSaveSettingsParamsPostSchema = { enum: ['autoTranslate', 'autoTranslateLanguage'], }, value: { - type: ['boolean', 'string'], + anyOf: [{ type: 'boolean' }, { type: 'string' }], }, defaultLanguage: { type: 'string', diff --git a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts index c8bb88cfc1c6..e25dfb0ce2fb 100644 --- a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts +++ b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts @@ -12,6 +12,7 @@ export type ChannelsCreateProps = { encrypted?: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const channelsCreatePropsSchema = { diff --git a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts index a63d37da07ba..a6009fe20d85 100644 --- a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts +++ b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts @@ -11,7 +11,7 @@ const FederationVerifyMatrixIdPropsSchema = { properties: { matrixIds: { type: 'array', - items: [{ type: 'string' }], + items: { type: 'string' }, uniqueItems: true, }, }, diff --git a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts index c34a720bd4b7..7c3781d787c4 100644 --- a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts @@ -14,6 +14,7 @@ export type GroupsCreateProps = { encrypted: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const GroupsCreatePropsSchema = { diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts index e9ef650656cd..249a12096729 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts @@ -1,4 +1,4 @@ -import type { OutgoingIntegrationEvent } from '@rocket.chat/core-typings'; +import type { OutgoingIntegrationEvent, IntegrationScriptEngine } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv(); @@ -16,6 +16,7 @@ export type IntegrationsCreateProps = alias?: string; avatar?: string; emoji?: string; + scriptEngine?: IntegrationScriptEngine; } | { type: 'webhook-outgoing'; @@ -44,6 +45,7 @@ export type IntegrationsCreateProps = alias?: string; avatar?: string; emoji?: string; + scriptEngine?: IntegrationScriptEngine; }; const integrationsCreateSchema = { @@ -96,6 +98,10 @@ const integrationsCreateSchema = { type: 'string', nullable: true, }, + scriptEngine: { + type: 'string', + nullable: true, + }, }, required: ['type', 'username', 'channel', 'scriptEnabled', 'name', 'enabled'], additionalProperties: false, @@ -196,6 +202,10 @@ const integrationsCreateSchema = { type: 'string', nullable: true, }, + scriptEngine: { + type: 'string', + nullable: true, + }, }, required: ['type', 'username', 'channel', 'event', 'scriptEnabled', 'name', 'enabled'], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index c6d102a967e4..87c0106f6d3f 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicense } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3, LicenseLimitKind } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -22,9 +22,35 @@ const licensesAddPropsSchema = { export const isLicensesAddProps = ajv.compile(licensesAddPropsSchema); +type licensesInfoProps = { + loadValues?: boolean; +}; + +const licensesInfoPropsSchema = { + type: 'object', + properties: { + loadValues: { + type: 'boolean', + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLicensesInfoProps = ajv.compile(licensesInfoPropsSchema); + export type LicensesEndpoints = { '/v1/licenses.get': { - GET: () => { licenses: Array }; + GET: () => { licenses: Array }; + }; + '/v1/licenses.info': { + GET: (params: licensesInfoProps) => { + data: { + license: ILicenseV3 | undefined; + activeModules: string[]; + limits: Record; + }; + }; }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; diff --git a/packages/rest-typings/src/v1/misc.ts b/packages/rest-typings/src/v1/misc.ts index 4af37334e287..804b72a763de 100644 --- a/packages/rest-typings/src/v1/misc.ts +++ b/packages/rest-typings/src/v1/misc.ts @@ -164,6 +164,22 @@ const MethodCallAnonSchema = { export const isMethodCallAnonProps = ajv.compile(MethodCallAnonSchema); +type Fingerprint = { setDeploymentAs: 'new-workspace' | 'updated-configuration' }; + +const FingerprintSchema = { + type: 'object', + properties: { + setDeploymentAs: { + type: 'string', + enum: ['new-workspace', 'updated-configuration'], + }, + }, + required: ['setDeploymentAs'], + additionalProperties: false, +}; + +export const isFingerprintProps = ajv.compile(FingerprintSchema); + type PwGetPolicyReset = { token: string }; const PwGetPolicyResetSchema = { @@ -229,6 +245,12 @@ export type MiscEndpoints = { }; }; + '/v1/fingerprint': { + POST: (params: Fingerprint) => { + success: boolean; + }; + }; + '/v1/smtp.check': { GET: () => { isSMTPConfigured: boolean; diff --git a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts index 69b1d85f22a5..48b859a7899b 100644 --- a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts +++ b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts @@ -10,6 +10,7 @@ type ReportHistoryProps = { export type ReportHistoryPropsGET = PaginatedRequest; const reportHistoryPropsSchema = { + type: 'object', properties: { latest: { type: 'string', diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index f85358c38ee9..bebea2856861 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2550,6 +2550,10 @@ const GETLivechatRoomsParamsSchema = { type: 'string', nullable: true, }, + query: { + type: 'string', + nullable: true, + }, fields: { type: 'string', nullable: true, @@ -2582,12 +2586,16 @@ const GETLivechatRoomsParamsSchema = { nullable: true, }, open: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, onhold: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, tags: { type: 'array', @@ -3112,7 +3120,7 @@ const POSTLivechatAppearanceParamsSchema = { type: 'string', }, value: { - type: ['string', 'boolean', 'number'], + anyOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'number' }], }, }, required: ['_id', 'value'], @@ -3588,6 +3596,7 @@ export type OmnichannelEndpoints = { }; '/v1/livechat/triggers/:_id': { GET: () => { trigger: ILivechatTrigger | null }; + DELETE: () => void; }; '/v1/livechat/rooms': { GET: (params: GETLivechatRoomsParams) => PaginatedResult<{ rooms: IOmnichannelRoom[] }>; @@ -3607,7 +3616,7 @@ export type OmnichannelEndpoints = { }>; }; '/v1/livechat/integrations.settings': { - GET: () => { settings: ISetting[] }; + GET: () => { settings: ISetting[]; success: boolean }; }; '/v1/livechat/upload/:rid': { POST: (params: { file: File }) => IMessage & { newRoom: boolean; showConnecting: boolean }; @@ -3804,4 +3813,7 @@ export type OmnichannelEndpoints = { '/v1/livechat/analytics/dashboards/conversations-by-agent': { GET: (params: GETDashboardConversationsByType) => ReportWithUnmatchingElements; }; + '/v1/livechat/webhook.test': { + POST: () => void; + }; }; diff --git a/packages/server-cloud-communication/.eslintrc.json b/packages/server-cloud-communication/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/server-cloud-communication/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/server-cloud-communication/package.json b/packages/server-cloud-communication/package.json new file mode 100644 index 000000000000..52a3ff801dac --- /dev/null +++ b/packages/server-cloud-communication/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rocket.chat/server-cloud-communication", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@rocket.chat/license": "workspace:^", + "@types/jest": "~29.5.3", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "~29.0.5", + "typescript": "~5.1.6" + }, + "volta": { + "extends": "../../package.json" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": [ + "/dist" + ] +} diff --git a/packages/server-cloud-communication/src/definitions/index.ts b/packages/server-cloud-communication/src/definitions/index.ts new file mode 100644 index 000000000000..d554aa538059 --- /dev/null +++ b/packages/server-cloud-communication/src/definitions/index.ts @@ -0,0 +1,40 @@ +type Dictionary = { [lng: string]: Record }; + +type Message = { + remainingDays: number; + title: 'message_token'; + subtitle: 'message_token'; + description: 'message_token'; + type: 'info' | 'alert' | 'error'; + params: Record & { + instance_ws_name: string; + instance_domain: string; + remaining_days: number; + }; + link: string; +}; + +type Version = { + version: string; + expiration: Date; + messages?: Message[]; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface SupportedVersions { + timestamp: string; + messages?: Message[]; + versions: Version[]; + exceptions?: { + domain: string; + uniqueId: string; + messages?: Message[]; + versions: Version[]; + }; + i18n?: Dictionary; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface SignedSupportedVersions extends SupportedVersions { + signed: string; // SerializedJWT; +} diff --git a/packages/server-cloud-communication/src/index.ts b/packages/server-cloud-communication/src/index.ts new file mode 100644 index 000000000000..382400b0c72c --- /dev/null +++ b/packages/server-cloud-communication/src/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { SupportedVersions, SignedSupportedVersions } from './definitions'; + +export { SupportedVersions, SignedSupportedVersions }; diff --git a/packages/server-cloud-communication/tsconfig.json b/packages/server-cloud-communication/tsconfig.json new file mode 100644 index 000000000000..e2be47cf5499 --- /dev/null +++ b/packages/server-cloud-communication/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md new file mode 100644 index 000000000000..b5d9e6f419d4 --- /dev/null +++ b/packages/tools/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/tools + +## 0.1.0 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations + +## 0.1.0-rc.0 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations diff --git a/packages/tools/package.json b/packages/tools/package.json index d9ab70550ce4..ed5e7bdd44bf 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/tools", - "version": "0.0.1", + "version": "0.1.0", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 261823100d0a..b8bc90d9cb54 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,3 +1,4 @@ export * from './pick'; export * from './timezone'; export * from './stream'; +export * from './wrapExceptions'; diff --git a/packages/tools/src/wrapExceptions.ts b/packages/tools/src/wrapExceptions.ts new file mode 100644 index 000000000000..bd830a92bfeb --- /dev/null +++ b/packages/tools/src/wrapExceptions.ts @@ -0,0 +1,46 @@ +const isPromise = (value: unknown): value is Promise => !!value && value instanceof Promise; + +export function wrapExceptions( + getter: () => T, +): { + catch: (errorWrapper: (error: any) => T) => T; + suppress: (errorWrapper?: (error: any) => void) => T | undefined; +}; +export function wrapExceptions( + getter: () => Promise, +): { + catch: (errorWrapper: (error: any) => T | Awaited) => Promise; + suppress: (errorWrapper?: (error: any) => void) => Promise; +}; +export function wrapExceptions(getter: () => T) { + const doCatch = (errorWrapper: (error: any) => T | Awaited): T => { + try { + const value = getter(); + if (isPromise(value)) { + return value.catch(errorWrapper) as T; + } + + return value; + } catch (error) { + return errorWrapper(error); + } + }; + + const doSuppress = (errorWrapper?: (error: any) => void) => { + try { + const value = getter(); + if (isPromise(value)) { + return value.catch((error) => errorWrapper?.(error)); + } + + return value; + } catch (error) { + errorWrapper?.(error); + } + }; + + return { + catch: doCatch, + suppress: doSuppress, + }; +} diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index fa6ebadcedfa..77ab6ff504ca 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,87 @@ # @rocket.chat/ui-client +## 2.0.0 + +### Minor Changes + +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- ee3815fce4: feat: add ChangePassword field to Account/Security + +### Patch Changes + +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- Updated dependencies [074db3b419] +- Updated dependencies [b8f3d5014f] + - @rocket.chat/ui-contexts@2.0.0 + +## 2.0.0-rc.5 + +### Patch Changes + +- @rocket.chat/ui-contexts@2.0.0-rc.5 + +## 2.0.0-rc.4 + +### Patch Changes + +- @rocket.chat/ui-contexts@2.0.0-rc.4 + +## 2.0.0-rc.3 + +### Patch Changes + +- @rocket.chat/ui-contexts@2.0.0-rc.3 + +## 2.0.0-rc.2 + +### Patch Changes + +- @rocket.chat/ui-contexts@2.0.0-rc.2 + +## 2.0.0-rc.1 + +### Patch Changes + +- @rocket.chat/ui-contexts@2.0.0-rc.1 + +## 2.0.0-rc.0 + +### Minor Changes + +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- ee3815fce4: feat: add ChangePassword field to Account/Security + +### Patch Changes + +- b8f3d5014f: Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. +- Updated dependencies [074db3b419] +- Updated dependencies [b8f3d5014f] + - @rocket.chat/ui-contexts@2.0.0-rc.0 + +## 1.0.8 + +### Patch Changes + +- @rocket.chat/ui-contexts@1.0.8 + +## 1.0.7 + +### Patch Changes + +- @rocket.chat/ui-contexts@1.0.7 + +## 1.0.6 + +### Patch Changes + +- @rocket.chat/ui-contexts@1.0.6 + +## 1.0.5 + +### Patch Changes + +- @rocket.chat/ui-contexts@1.0.5 + ## 1.0.4 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index adf07e87c9a0..fa227575ccc2 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,13 +1,13 @@ { "name": "@rocket.chat/ui-client", - "version": "1.0.4", + "version": "2.0.0", "private": true, "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage-hooks": "^0.32.1", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/ui-contexts": "workspace:~", "@storybook/addon-actions": "~6.5.16", @@ -61,7 +61,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "1.0.4", + "@rocket.chat/ui-contexts": "2.0.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/src/components/CustomFieldsForm.tsx b/packages/ui-client/src/components/CustomFieldsForm.tsx index 9423456ebe8e..0d1ba2ca5b1b 100644 --- a/packages/ui-client/src/components/CustomFieldsForm.tsx +++ b/packages/ui-client/src/components/CustomFieldsForm.tsx @@ -1,6 +1,6 @@ import type { CustomFieldMetadata } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, Select, TextInput } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError, Select, TextInput } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -73,10 +73,10 @@ const CustomField = ({ rules={{ minLength: props.minLength, maxLength: props.maxLength, validate: { required: validateRequired } }} render={({ field }) => ( - + {label || t(name as TranslationKey)} - - + + ({ options={selectOptions as SelectOption[]} flexGrow={1} /> - - +
+ {errorMessage} - +
)} /> diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index 1420a62346d6..6c5e12be8622 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -57,7 +57,6 @@ type DropDownProps = { selectedOptionsTitle: TranslationKey; selectedOptions: OptionProp[]; setSelectedOptions: Dispatch>; - customSetSelected: Dispatch>; searchBarText?: TranslationKey; }; @@ -67,7 +66,6 @@ export const MultiSelectCustom = ({ selectedOptionsTitle, selectedOptions, setSelectedOptions, - customSetSelected, searchBarText, }: DropDownProps): ReactElement => { const reference = useRef(null); @@ -90,26 +88,15 @@ export const MultiSelectCustom = ({ const onSelect = (item: OptionProp, e?: FormEvent): void => { e?.stopPropagation(); - item.checked = !item.checked; if (item.checked === true) { - // the user has enabled this option -> add it to the selected options setSelectedOptions([...new Set([...selectedOptions, item])]); - customSetSelected((prevItems) => { - const newItems = prevItems; - const toggledItem = newItems.find(({ id }) => id === item.id); - - if (toggledItem) { - toggledItem.checked = !toggledItem.checked; - } - - return [...prevItems]; - }); - } else { - // the user has disabled this option -> remove this from the selected options list - setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id)); + return; } + + // the user has disabled this option -> remove this from the selected options list + setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id)); }; const count = dropdownOptions.filter((option) => option.checked).length; diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index 7e6bfdb9fee1..d8f8d60d8096 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -2,7 +2,7 @@ import { Box, CheckBox, Icon, Option, SearchInput, Tile } from '@rocket.chat/fus import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; -import { Fragment, useCallback, useEffect, useState } from 'react'; +import { Fragment, useCallback, useState } from 'react'; import type { OptionProp } from './MultiSelectCustom'; import { useFilteredOptions } from './useFilteredOptions'; @@ -19,13 +19,10 @@ const MultiSelectCustomList = ({ const t = useTranslation(); const [text, setText] = useState(''); - const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - - const [optionSearch, setOptionSearch] = useState(''); - useEffect(() => setOptionSearch(text), [setOptionSearch, text]); + const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - const filteredOptions = useFilteredOptions(optionSearch, options); + const filteredOptions = useFilteredOptions(text, options); return ( @@ -48,11 +45,11 @@ const MultiSelectCustomList = ({ {t(option.text as TranslationKey)}
) : ( -