diff --git a/.changeset/blue-ladybugs-raise.md b/.changeset/blue-ladybugs-raise.md deleted file mode 100644 index 44d7a06b4111a..0000000000000 --- 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 0000000000000..914f248cd8210 --- /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/bright-snakes-vanish.md b/.changeset/bright-snakes-vanish.md deleted file mode 100644 index f198bfe93ae93..0000000000000 --- 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 6b69289177b5e..0000000000000 --- 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 0000000000000..a7907979881b9 --- /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 17a0f9eb6dc52..0000000000000 --- 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 cb0887db08834..0000000000000 --- 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 07760541628ac..0000000000000 --- 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/cool-zoos-move.md b/.changeset/cool-zoos-move.md new file mode 100644 index 0000000000000..dda6fbe2b02e6 --- /dev/null +++ b/.changeset/cool-zoos-move.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed threads breaking when sending messages too fast diff --git a/.changeset/cuddly-houses-tie.md b/.changeset/cuddly-houses-tie.md deleted file mode 100644 index 76d86a6903888..0000000000000 --- 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 d912d2969d759..0000000000000 --- 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 67d453ab72458..0000000000000 --- 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 a9a797f35bc81..0000000000000 --- 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 935c12aebe859..0000000000000 --- 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 0000000000000..f5a673cd8c303 --- /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 6410813d80a6c..0000000000000 --- 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-gorillas-deliver.md b/.changeset/eleven-gorillas-deliver.md new file mode 100644 index 0000000000000..403bd294828b7 --- /dev/null +++ b/.changeset/eleven-gorillas-deliver.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix trying to upload same file again and again. diff --git a/.changeset/eleven-icons-tan.md b/.changeset/eleven-icons-tan.md deleted file mode 100644 index c51124c05dd26..0000000000000 --- 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 4a55f82d0abf5..0000000000000 --- 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 7dfb74955a945..0000000000000 --- 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 2374776bf3b57..0000000000000 --- 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 60dd920301632..0000000000000 --- 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 6c09cf6869c82..0000000000000 --- 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 f90513b946c38..0000000000000 --- 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 09437a2cb88ea..0000000000000 --- 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 0000000000000..db93491b0ecd4 --- /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 b23825d5a02a6..0000000000000 --- 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/friendly-glasses-mate.md b/.changeset/friendly-glasses-mate.md deleted file mode 100644 index 6a7a7b4f8546d..0000000000000 --- 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 cf77bbde55075..0000000000000 --- 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 c6af54a2ef579..0000000000000 --- 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 0000000000000..8d5f12b3a2868 --- /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 a8908b68a23ef..0000000000000 --- 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/good-elephants-live.md b/.changeset/good-elephants-live.md deleted file mode 100644 index 8cb3e9d87fc46..0000000000000 --- 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 b07f5ea3e6bfb..0000000000000 --- 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 28673ce91a73c..0000000000000 --- 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 0000000000000..c04e52fb48a03 --- /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 5c32965dcf624..0000000000000 --- 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 893f533521142..0000000000000 --- 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 79dfba6dd031c..0000000000000 --- 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 7100fec026e3e..0000000000000 --- 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 679f46fb84202..0000000000000 --- 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 1fd017e7fc163..0000000000000 --- 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/khaki-feet-dance.md b/.changeset/khaki-feet-dance.md new file mode 100644 index 0000000000000..a419afa341431 --- /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 0000000000000..40ce15453ff49 --- /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 554c1c1204ead..0000000000000 --- 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 0000000000000..19f1eade9a9bb --- /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 080e9986cebba..0000000000000 --- 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-sheep-try.md b/.changeset/loud-sheep-try.md deleted file mode 100644 index f82d0d0695547..0000000000000 --- 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 4e28c6a43c208..0000000000000 --- 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-hounds-sing.md b/.changeset/lucky-hounds-sing.md deleted file mode 100644 index 20b09afaf5459..0000000000000 --- 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/lucky-vans-develop.md b/.changeset/lucky-vans-develop.md new file mode 100644 index 0000000000000..e57b7a1e68d54 --- /dev/null +++ b/.changeset/lucky-vans-develop.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with file attachments in rooms' messages export having no content diff --git a/.changeset/many-icons-provide.md b/.changeset/many-icons-provide.md deleted file mode 100644 index bf82407980ad5..0000000000000 --- 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 54b2846901dea..0000000000000 --- 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 b8b372306d0ed..0000000000000 --- 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 6c307604eaa91..0000000000000 --- 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 0000000000000..dfc9d763e1c00 --- /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 bf5dc72e6cc0b..0000000000000 --- 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 0000000000000..aaddc5d51a380 --- /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 fa9879d844262..0000000000000 --- 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/old-zoos-hang.md b/.changeset/old-zoos-hang.md new file mode 100644 index 0000000000000..eb39a6c9d83cd --- /dev/null +++ b/.changeset/old-zoos-hang.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: mobile ringing notification missing call id diff --git a/.changeset/perfect-adults-travel.md b/.changeset/perfect-adults-travel.md deleted file mode 100644 index 61ae4ab6dad5d..0000000000000 --- 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 dcc1088de0b53..0000000000000 --- 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/popular-actors-cheat.md b/.changeset/popular-actors-cheat.md new file mode 100644 index 0000000000000..aad5ec6ae6384 --- /dev/null +++ b/.changeset/popular-actors-cheat.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Do not allow auto-translation to be enabled in E2E rooms diff --git a/.changeset/pretty-bees-give.md b/.changeset/pretty-bees-give.md deleted file mode 100644 index 8891420308c52..0000000000000 --- 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 0000000000000..cad8bc8bfa329 --- /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 7a6d7b4446547..0000000000000 --- 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 0000000000000..f2735e615491b --- /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 a6222cba16c96..0000000000000 --- 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 86c2d72832234..0000000000000 --- 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 d6531285597cf..0000000000000 --- 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 48a82b5902cb3..0000000000000 --- 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 f915aa38f758f..0000000000000 --- 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 0000000000000..3ca321bd392fb --- /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 ccdc3c94dda43..0000000000000 --- 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-geckos-drive.md b/.changeset/serious-geckos-drive.md deleted file mode 100644 index 454337399772d..0000000000000 --- a/.changeset/serious-geckos-drive.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/ui-client': minor -'@rocket.chat/meteor': minor ---- - -Added Reports Metrics Dashboard to Omnichannel diff --git a/.changeset/serious-shrimps-try.md b/.changeset/serious-shrimps-try.md deleted file mode 100644 index 114293aa104eb..0000000000000 --- 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 0000000000000..46fd1b7ddb620 --- /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/shaggy-beans-poke.md b/.changeset/shaggy-beans-poke.md deleted file mode 100644 index 31a4806389529..0000000000000 --- 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 117063d93f6fb..0000000000000 --- 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 a12817ed175b4..9a85d37a2f9d3 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 f024eca38d04d..0000000000000 --- 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 1c28ce7bad11f..0000000000000 --- 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 aab23e14e5f19..0000000000000 --- 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 be74b1bef215e..0000000000000 --- 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 f99bcfb71c304..0000000000000 --- 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 b28de342b2748..0000000000000 --- 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 fd773b17f5c81..0000000000000 --- 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 67fdff5ca7586..0000000000000 --- 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 9ad4239f03428..0000000000000 --- 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 0000000000000..6fcb205064833 --- /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 c326eb7dca70b..0000000000000 --- 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 d907c063f5687..0000000000000 --- 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 1c1eaa3173a81..0000000000000 --- 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-masks-learn.md b/.changeset/stale-masks-learn.md new file mode 100644 index 0000000000000..1523b02b0c951 --- /dev/null +++ b/.changeset/stale-masks-learn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/server-fetch': patch +--- + +Fixed an issue where the payload of an HTTP request made by an app wouldn't be correctly encoded in some cases diff --git a/.changeset/stale-roses-knock.md b/.changeset/stale-roses-knock.md deleted file mode 100644 index 25e93fa8c3460..0000000000000 --- 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 0000000000000..ca194dd2f9d42 --- /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/sweet-chefs-exist.md b/.changeset/sweet-chefs-exist.md new file mode 100644 index 0000000000000..6ceee63dd7625 --- /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 0000000000000..f7da740ebcc0d --- /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 4af3bddd875be..0000000000000 --- 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 6e3057775c328..0000000000000 --- 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 8cb729531fae2..0000000000000 --- 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 0000000000000..9d4095e7771ba --- /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/bright-carpets-fly.md b/.changeset/thirty-pumpkins-fix.md similarity index 50% rename from .changeset/bright-carpets-fly.md rename to .changeset/thirty-pumpkins-fix.md index 6a8ac2608569a..11b92b064e159 100644 --- a/.changeset/bright-carpets-fly.md +++ b/.changeset/thirty-pumpkins-fix.md @@ -1,7 +1,8 @@ --- '@rocket.chat/core-typings': minor '@rocket.chat/rest-typings': minor +'@rocket.chat/tools': minor '@rocket.chat/meteor': minor --- -new: ring mobile users on direct conference calls +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 0ce911d9f6faf..0000000000000 --- 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 0000000000000..cff12f3dc7d31 --- /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 3c2013f790234..0000000000000 --- 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 a146bd6a0eae5..0000000000000 --- 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 0000000000000..f89564a9b53c0 --- /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-apples-turn.md b/.changeset/tough-apples-turn.md new file mode 100644 index 0000000000000..056a0645186e2 --- /dev/null +++ b/.changeset/tough-apples-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Forward headers when using proxy for file uploads diff --git a/.changeset/tough-carrots-walk.md b/.changeset/tough-carrots-walk.md new file mode 100644 index 0000000000000..2851e697b85e9 --- /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 2ab1254525b2a..0000000000000 --- 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 0000000000000..123bf0a7764b4 --- /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 fffa51020e30d..0000000000000 --- 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 a896a7c12ee4c..0000000000000 --- 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 db48243c40ed4..0000000000000 --- 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 f81cf1efbe92e..0000000000000 --- 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 0000000000000..5b187b8a7f11e --- /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 24395a78f85d2..0000000000000 --- 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 6b18eb4976866..0000000000000 --- 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 cd8f832b18351..e793bc978902d 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/wicked-jars-double.md b/.changeset/wicked-jars-double.md new file mode 100644 index 0000000000000..23deffe8606fa --- /dev/null +++ b/.changeset/wicked-jars-double.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Handle the username update in the background diff --git a/.changeset/wild-spiders-smell.md b/.changeset/wild-spiders-smell.md deleted file mode 100644 index 9694d6259d3ac..0000000000000 --- 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 cb5c731fb6fbe..0000000000000 --- 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-ways-fetch.md b/.changeset/wise-ways-fetch.md deleted file mode 100644 index a81063813c35a..0000000000000 --- 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 faaa5d44c134e..0000000000000 --- 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-schools-tell.md b/.changeset/yellow-schools-tell.md deleted file mode 100644 index c1040fa0856a1..0000000000000 --- 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/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml new file mode 100644 index 0000000000000..284a0985b78e5 --- /dev/null +++ b/.github/actions/build-docker/action.yml @@ -0,0 +1,75 @@ +name: 'Meteor Docker' + +inputs: + CR_USER: + required: true + CR_PAT: + required: true + node-version: + required: true + description: 'Node version' + type: string + platform: + required: false + description: 'Platform' + type: string + +runs: + using: composite + + steps: + - name: Login to GitHub Container Registry + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ inputs.CR_USER }} + password: ${{ inputs.CR_PAT }} + + - name: Restore build + uses: actions/download-artifact@v3 + with: + name: build + path: /tmp/build + + - name: Unpack build + shell: bash + run: | + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz + + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + cache-modules: true + install: true + + - run: yarn build + shell: bash + + - name: Build Docker images + shell: bash + run: | + args=(rocketchat) + + if [[ '${{ inputs.platform }}' = 'alpine' ]]; then + args+=($SERVICES_PUBLISH) + fi; + + 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) + + if [[ '${{ inputs.platform }}' = 'alpine' ]]; then + args+=($SERVICES_PUBLISH) + fi; + + docker compose -f docker-compose-ci.yml push "${args[@]}" diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml new file mode 100644 index 0000000000000..21fec059c8dec --- /dev/null +++ b/.github/actions/meteor-build/action.yml @@ -0,0 +1,129 @@ +name: 'Meteor Build' + +inputs: + coverage: + required: false + description: 'Enable coverage' + type: boolean + reset-meteor: + required: false + description: 'Reset Meteor' + type: boolean + node-version: + required: true + description: 'Node version' + type: string + +runs: + using: composite + + steps: + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 4 + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + cache-modules: true + install: true + + # - name: Free disk space + # run: | + # sudo apt clean + # docker rmi $(docker image ls -aq) + # df -h + + - name: Cache vite + uses: actions/cache@v3 + with: + path: ./node_modules/.vite + key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} + restore-keys: | + vite-local-cache-${{ runner.os }}- + + - name: Cache meteor local + uses: actions/cache@v3 + with: + path: ./apps/meteor/.meteor/local + key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} + restore-keys: | + meteor-local-cache-${{ runner.os }}- + + - name: Cache meteor + uses: actions/cache@v3 + with: + path: ~/.meteor + key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} + restore-keys: | + meteor-cache-${{ runner.os }}- + + - name: Install Meteor + shell: bash + run: | + # Restore bin from cache + set +e + METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) + METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") + set -e + LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor + if [ -e $LAUNCHER ] + then + echo "Cached Meteor bin found, restoring it" + sudo cp "$LAUNCHER" "/usr/local/bin/meteor" + else + echo "No cached Meteor bin found." + fi + + # only install meteor if bin isn't found + command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + + - name: Versions + shell: bash + run: | + npm --versions + yarn -v + node -v + meteor --version + meteor npm --versions + meteor node -v + git version + + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Translation check + shell: bash + run: yarn turbo run translation-check + + - name: Reset Meteor + shell: bash + if: ${{ inputs.reset-meteor == 'true' }} + working-directory: ./apps/meteor + run: meteor reset + + - name: Build Rocket.Chat From Pull Request + shell: bash + if: startsWith(github.ref, 'refs/pull/') == true + env: + METEOR_PROFILE: 1000 + BABEL_ENV: ${{ inputs.coverage == 'true' && 'coverage' || '' }} + run: yarn build:ci -- --directory /tmp/dist + + - name: Build Rocket.Chat + shell: bash + if: startsWith(github.ref, 'refs/pull/') != true + run: yarn build:ci -- --directory /tmp/dist + + - name: Prepare build + shell: bash + run: | + cd /tmp/dist + tar czf /tmp/Rocket.Chat.tar.gz bundle + + - name: Store build + uses: actions/upload-artifact@v3 + with: + name: build + path: /tmp/Rocket.Chat.tar.gz diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 5a556a1a8e298..57cdac047423d 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 9a05543605dbc..d77966f186b3e 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -57,6 +57,8 @@ on: required: false REPORTER_ROCKETCHAT_API_KEY: required: false + CODECOV_TOKEN: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -95,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 @@ -237,6 +247,7 @@ jobs: directory: ./apps/meteor flags: e2e verbose: true + token: ${{ secrets.CODECOV_TOKEN }} - name: Store e2e-ee-coverage if: inputs.type == 'ui' && inputs.release == 'ee' diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 03c6bc2352abc..b4ef5cb273ad5 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + secrets: + CODECOV_TOKEN: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -36,3 +39,4 @@ jobs: with: flags: unit verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fe46ecf66c36..ec8e905cd8035 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,7 +123,7 @@ jobs: run: yarn build build: - name: 📦 Meteor Build + name: 📦 Meteor Build - coverage needs: [release-versions, packages-build] runs-on: ubuntu-20.04 @@ -138,114 +138,40 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat $GITHUB_EVENT_PATH - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 4 - - uses: actions/checkout@v3 - - name: Setup NodeJS - uses: ./.github/actions/setup-node + - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} - cache-modules: true - install: true - - # - name: Free disk space - # run: | - # sudo apt clean - # docker rmi $(docker image ls -aq) - # df -h - - - name: Cache vite - uses: actions/cache@v3 - with: - path: ./node_modules/.vite - key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} - restore-keys: | - vite-local-cache-${{ runner.os }}- - - - name: Cache meteor local - uses: actions/cache@v3 - with: - path: ./apps/meteor/.meteor/local - key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} - restore-keys: | - meteor-local-cache-${{ runner.os }}- + coverage: true - - name: Cache meteor - uses: actions/cache@v3 - with: - path: ~/.meteor - key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} - restore-keys: | - meteor-cache-${{ runner.os }}- - - - name: Install Meteor - run: | - # Restore bin from cache - set +e - METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) - METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") - set -e - LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor - if [ -e $LAUNCHER ] - then - echo "Cached Meteor bin found, restoring it" - sudo cp "$LAUNCHER" "/usr/local/bin/meteor" - else - echo "No cached Meteor bin found." - fi - - # only install meteor if bin isn't found - command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + build-prod: + name: 📦 Meteor Build - official + needs: [tests-done, release-versions, packages-build] + if: (github.event_name == 'release' || github.ref == 'refs/heads/develop') + runs-on: ubuntu-20.04 - - name: Versions + steps: + - name: Github Info run: | - npm --versions - yarn -v - node -v - meteor --version - meteor npm --versions - meteor node -v - git version - - - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - - - name: Translation check - run: yarn turbo run translation-check - - - name: Reset Meteor - if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' - working-directory: ./apps/meteor - run: meteor reset - - - name: Build Rocket.Chat From Pull Request - if: startsWith(github.ref, 'refs/pull/') == true - env: - METEOR_PROFILE: 1000 - run: yarn build:ci -- --directory /tmp/dist - - - name: Build Rocket.Chat - if: startsWith(github.ref, 'refs/pull/') != true - run: yarn build:ci -- --directory /tmp/dist + echo "GITHUB_ACTION: $GITHUB_ACTION" + echo "GITHUB_ACTOR: $GITHUB_ACTOR" + echo "GITHUB_REF: $GITHUB_REF" + echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF" + echo "GITHUB_BASE_REF: $GITHUB_BASE_REF" + echo "github.event_name: ${{ github.event_name }}" + cat $GITHUB_EVENT_PATH - - name: Prepare build - run: | - cd /tmp/dist - tar czf /tmp/Rocket.Chat.tar.gz bundle + - uses: actions/checkout@v3 - - name: Store build - uses: actions/upload-artifact@v3 + - uses: ./.github/actions/meteor-build with: - name: build - path: /tmp/Rocket.Chat.tar.gz + node-version: ${{ needs.release-versions.outputs.node-version }} + coverage: false - build-gh-docker: + 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: @@ -263,55 +189,41 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ secrets.CR_USER }} - password: ${{ secrets.CR_PAT }} - - - name: Restore build - uses: actions/download-artifact@v3 - with: - name: build - path: /tmp/build - - - name: Unpack build - run: | - cd /tmp/build - tar xzf Rocket.Chat.tar.gz - rm Rocket.Chat.tar.gz - - - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - - - name: Setup NodeJS - uses: ./.github/actions/setup-node + # 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 }} node-version: ${{ needs.release-versions.outputs.node-version }} - cache-modules: true - install: true + platform: ${{ matrix.platform }} - - run: yarn build - - - name: Build Docker images - run: | - args=(rocketchat) - - if [[ '${{ matrix.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + build-gh-docker: + name: 🚢 Build Docker Images for Production + needs: [build-prod, release-versions] + runs-on: ubuntu-20.04 - docker compose -f docker-compose-ci.yml build "${args[@]}" + env: + RC_DOCKERFILE: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-dockerfile-alpine || needs.release-versions.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-docker-tag-alpine || needs.release-versions.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} + SERVICES_PUBLISH: 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service' - - name: Publish Docker images to GitHub Container Registry - run: | - args=(rocketchat) + strategy: + fail-fast: false + matrix: + platform: ['official', 'alpine'] - if [[ '${{ matrix.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + steps: + - uses: actions/checkout@v3 - docker compose -f docker-compose-ci.yml push "${args[@]}" + - uses: ./.github/actions/build-docker + with: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + node-version: ${{ needs.release-versions.outputs.node-version }} + platform: ${{ matrix.platform }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' @@ -337,10 +249,12 @@ jobs: uses: ./.github/workflows/ci-test-unit.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} test-api: name: 🔨 Test API (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -359,7 +273,7 @@ jobs: test-ui: name: 🔨 Test UI (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -385,7 +299,7 @@ jobs: test-api-ee: name: 🔨 Test API (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -407,7 +321,7 @@ jobs: test-ui-ee: name: 🔨 Test UI (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -431,6 +345,7 @@ jobs: QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-done: name: ✅ Tests Done @@ -443,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: [tests-done, release-versions] + 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 @@ -463,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"; @@ -506,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 @@ -752,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 0000000000000..2daeb533937db --- /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 0000000000000..501881370244f --- /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/.babelrc b/apps/meteor/.babelrc index a8c20b400ca53..382b93318fab4 100644 --- a/apps/meteor/.babelrc +++ b/apps/meteor/.babelrc @@ -1,9 +1,15 @@ { - "presets": [ - "@babel/preset-env", - "@babel/preset-react" - ], - "plugins": [ - "babel-plugin-istanbul" - ] + "presets": ["@babel/preset-env", "@babel/preset-react"], + "env": { + "coverage": { + "plugins": [ + [ + "istanbul", + { + "exclude": ["**/*.spec.js", "**/*.test.js"] + } + ] + ] + } + } } diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 62a0476d9077b..003baa57aa8b8 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 9f03ae9f3db4e..1481b0445e45f 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 287cb313c1743..a9fd54ab87117 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 97836558ab283..ae788af78034c 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 e8cfc7ec4c01f..6641d0478a104 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 da6de9efbde1b..66f61e2cd8ccc 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 182d30ddabb94..9c2e0b63e2404 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,626 @@ # @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 + +- db919f9b23: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- ebeb088441: fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times +- 8a7d5d3898: fix: agent role being removed upon user deactivation +- 759fe2472a: 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. +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/omnichannel-services@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/presence@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/cron@0.0.6 + - @rocket.chat/instance-status@0.0.10 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + - @rocket.chat/api-client@0.1.4 + - @rocket.chat/pdf-worker@0.0.10 + - @rocket.chat/gazzodown@1.0.4 + - @rocket.chat/ui-contexts@1.0.4 + - @rocket.chat/fuselage-ui-kit@1.0.4 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.4 + - @rocket.chat/ui-video-conf@1.0.4 + - @rocket.chat/web-ui-registration@1.0.4 + ## 6.3.3 ### Patch Changes diff --git a/apps/meteor/app/api/server/default/info.ts b/apps/meteor/app/api/server/default/info.ts index b7806ab08f320..8297f90fffd98 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 0000000000000..ca55cfa33e3e8 --- /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 39f4b82b350b9..53ba3656babed 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 70b7fc875082a..8e0541b8040bc 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 02fc30763eeb4..7be5b1fc13fe0 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 df54b683fda4c..8f2999cee71eb 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 dec4da6bf87b0..ae5a79719cce3 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/users.ts b/apps/meteor/app/api/server/v1/users.ts index a4e3f974ac654..b23d41255c3b3 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -126,7 +126,9 @@ API.v1.addRoute( realname: this.bodyParams.data.name, username: this.bodyParams.data.username, nickname: this.bodyParams.data.nickname, + bio: this.bodyParams.data.bio, statusText: this.bodyParams.data.statusText, + statusType: this.bodyParams.data.statusType, newPassword: this.bodyParams.data.newPassword, typedPassword: this.bodyParams.data.currentPassword, }; diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index 6ffd0005c764d..e1ee82d724789 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 71f7387e1aa5d..0ace08bb84466 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), ), ); @@ -288,7 +288,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await Livechat.getRoomMessages({ rid: roomId }); + const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); } diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 481292d61790b..91b0049513f0e 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 ae38feff5eff9..9055342128362 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 ba288c96d7b81..a9f5d450efad6 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 fc917028c33f4..7b5f1594e5c31 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/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 1ba5bcdfcd767..e396d78887a91 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -46,6 +46,13 @@ Meteor.methods({ switch (field) { case 'autoTranslate': + const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); + if (room && value === '1') { + throw new Meteor.Error('error-e2e-enabled', 'Enabling auto-translation in E2E encrypted rooms is not allowed', { + method: 'saveAutoTranslateSettings', + }); + } + await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); if (!subscription.autoTranslateLanguage && options.defaultLanguage) { await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index 3e9c9dbd8a35a..f885a23b8e6b1 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/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts index dc57307b1c4cf..ed07540ba2b0b 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts @@ -1,7 +1,7 @@ import { Message } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { isRegisterUser } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; @@ -25,5 +25,9 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean await Message.saveSystemMessage(type, rid, user.username, user); } + + if (encrypted) { + await Subscriptions.disableAutoTranslateByRoomId(rid); + } return update; }; diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index d65897b720945..c2bd91e82dd86 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 c72a96297f376..0000000000000 --- 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 780aa5c67a99e..61b3a77966e79 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 4a35c9834ba58..2c5d9dec77dcb 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 1a69d108ae4cf..b495e3342d4b3 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 4a0c4b5fe394e..88509902cb6d6 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 f3b6dfc4238aa..639f29402fe9d 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 275e646e53430..f9f0cfadc6697 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 db425d2e8a309..7ee02a5e5de4e 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 2a04aa54cfe76..ce415d2aa9833 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 55698f4d27af5..0291534ac6379 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 de9fafc990655..5f5df80d0d3db 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 3afe84c409ec1..382478db61c7c 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 0000000000000..183065fd92a62 --- /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 0000000000000..f0683535de6ba --- /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 0000000000000..577abd4383d08 --- /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 9337fd0a01726..0000000000000 --- 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 0000000000000..f3885c1e95c2b --- /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 0000000000000..c8b07f8826cfd --- /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 0000000000000..bdd898b510f7f --- /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 0000000000000..d5f86fad8409f --- /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 0000000000000..5f529a4892ecf --- /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 e03f96df679df..386137ced6040 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 4bb1f634978e2..c7e783d4d5aa4 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 d2fbac1af8815..1d328d0c213e4 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 03a42e45a17bb..cb6fa94273a28 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 4ff220b38b6f8..f881c15f98867 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 { @@ -42,7 +43,7 @@ class CustomSoundsClass { const audio = document.createElement('audio'); audio.id = getCustomSoundId(sound._id); - audio.preload = 'auto'; + audio.preload = 'none'; audio.appendChild(source); document.body.appendChild(audio); @@ -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 5eb2ef38e5b72..3ad61c4c42f06 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 b953d4658c854..0000000000000 --- 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 ce5c09947a60f..c3869f8ff9636 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/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index b97ff60d86d6a..567e5e5d71ebf 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -32,12 +32,15 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { const fileUrl = await this.store.getRedirectURL(file); - if (fileUrl) { - const request = /^https:/.test(fileUrl) ? https : http; - request.get(fileUrl, (fileRes) => fileRes.pipe(out)); - } else { + if (!fileUrl) { out.end(); + return; } + + const request = /^https:/.test(fileUrl) ? https : http; + return new Promise((resolve) => { + request.get(fileUrl, (fileRes) => fileRes.pipe(out).on('finish', () => resolve())); + }); }; const AmazonS3Uploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index 124bad4365a0d..41eb4350b8768 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -32,12 +32,15 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { const fileUrl = await this.store.getRedirectURL(file, false); - if (fileUrl) { - const request = /^https:/.test(fileUrl) ? https : http; - request.get(fileUrl, (fileRes) => fileRes.pipe(out)); - } else { + if (!fileUrl) { out.end(); + return; } + + const request = /^https:/.test(fileUrl) ? https : http; + return new Promise((resolve) => { + request.get(fileUrl, (fileRes) => fileRes.pipe(out).on('finish', () => resolve())); + }); }; const GoogleCloudStorageUploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/config/Webdav.ts b/apps/meteor/app/file-upload/server/config/Webdav.ts index fb8c1ca82ca46..901c74e9c149e 100644 --- a/apps/meteor/app/file-upload/server/config/Webdav.ts +++ b/apps/meteor/app/file-upload/server/config/Webdav.ts @@ -19,7 +19,9 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, }; const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { - (await this.store.getReadStream(file._id, file)).pipe(out); + return new Promise(async (resolve) => { + (await this.store.getReadStream(file._id, file)).pipe(out).on('finish', () => resolve()); + }); }; const WebdavUploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 8f929a17fe340..e512e5d09bfe8 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -562,7 +562,32 @@ export const FileUpload = { ) { res.setHeader('Content-Disposition', `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(fileName)}"`); - request.get(fileUrl, (fileRes) => fileRes.pipe(res)); + request.get(fileUrl, (fileRes) => { + if (fileRes.statusCode !== 200) { + res.setHeader('x-rc-proxyfile-status', String(fileRes.statusCode)); + res.setHeader('content-length', 0); + res.writeHead(500); + res.end(); + return; + } + + // eslint-disable-next-line prettier/prettier + const headersToProxy = [ + 'age', + 'cache-control', + 'content-length', + 'content-type', + 'date', + 'expired', + 'last-modified', + ]; + + headersToProxy.forEach((header) => { + fileRes.headers[header] && res.setHeader(header, String(fileRes.headers[header])); + }); + + fileRes.pipe(res); + }); }, generateJWTToFileUrls({ rid, userId, fileId }: { rid: string; userId: string; fileId: string }) { diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index f241879cdc674..1b596d625d9b4 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 e1db46729011b..5162fa54ad9c5 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 0000000000000..e46984a893ef2 --- /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 0000000000000..b4d11b9f4e8b9 --- /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 0000000000000..1bbefb6a2ee7a --- /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 0000000000000..77ce2475e8c29 --- /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 0000000000000..2c78b6d98a7ce --- /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 b122b22ff3557..b5050b8c4716d 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 0000000000000..9f7a3017108d2 --- /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 d9c2db78b62e3..398f811612795 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 0000000000000..c20dc9c594276 --- /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 0000000000000..9ba74404cf261 --- /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 0000000000000..5f7519d693467 --- /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 bf84957ba8eaf..45548a17a5659 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 b865c72e0ccae..5358e3233ce7c 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 9e5d29261b367..59879f99d475c 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 166badee823d4..9e62561ebf9af 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 6dc477a2926f4..835f59419ad58 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 660af823de9e4..4e29576cf3bb5 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 192139f96b7c7..312451f54845e 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/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index c04148c07ba89..0703b24d9210f 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -21,6 +21,7 @@ const defaultFields = { avatarETag: 1, extension: 1, federated: 1, + statusLivechat: 1, } as const; const fullFields = { diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts index 934014b794a18..11e4418c4510d 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 c743b21e72df9..3177a4a0060fd 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'; @@ -345,7 +346,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; } @@ -366,6 +367,7 @@ export const saveUser = async function (userId, userData) { _id: userData._id, username: userData.username, name: userData.name, + updateUsernameInBackground: true, })) ) { throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 2eb360e150c66..34ca0ca246db3 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -1,5 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; import { _setRealName } from './setRealName'; import { _setUsername } from './setUsername'; @@ -11,7 +13,17 @@ import { validateName } from './validateName'; * @param {object} changes changes to the user */ -export async function saveUserIdentity({ _id, name: rawName, username: rawUsername }: { _id: string; name?: string; username?: string }) { +export async function saveUserIdentity({ + _id, + name: rawName, + username: rawUsername, + updateUsernameInBackground = false, +}: { + _id: string; + name?: string; + username?: string; + updateUsernameInBackground?: boolean; // TODO: remove this +}) { if (!_id) { return false; } @@ -48,46 +60,91 @@ export async function saveUserIdentity({ _id, name: rawName, username: rawUserna // if coming from old username, update all references if (previousUsername) { - if (usernameChanged && typeof rawUsername !== 'undefined') { - const fileStore = FileUpload.getStore('Avatars'); - const previousFile = await fileStore.model.findOneByName(previousUsername); - const file = await fileStore.model.findOneByName(username); - if (file) { - await fileStore.model.deleteFile(file._id); - } - if (previousFile) { - await fileStore.model.updateFileNameById(previousFile._id, username); - } - - await Messages.updateAllUsernamesByUserId(user._id, username); - await Messages.updateUsernameOfEditByUserId(user._id, username); - - const cursor = Messages.findByMention(previousUsername); - for await (const msg of cursor) { - const updatedMsg = msg.msg.replace(new RegExp(`@${previousUsername}`, 'ig'), `@${username}`); - await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); - } - - await Rooms.replaceUsername(previousUsername, username); - await Rooms.replaceMutedUsername(previousUsername, username); - await Rooms.replaceUsernameOfUserByUserId(user._id, username); - await Subscriptions.setUserUsernameByUserId(user._id, username); - - await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); + const handleUpdateParams = { + username, + previousUsername, + rawUsername, + usernameChanged, + user, + name, + previousName, + rawName, + nameChanged, + }; + if (updateUsernameInBackground) { + setImmediate(async () => { + try { + await updateUsernameReferences(handleUpdateParams); + } catch (err) { + SystemLogger.error(err); + } + }); + } else { + await updateUsernameReferences(handleUpdateParams); } + } + + return true; +} - // update other references if either the name or username has changed - if (usernameChanged || nameChanged) { - // update name and fname of 1-on-1 direct messages - await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); +async function updateUsernameReferences({ + username, + previousUsername, + rawUsername, + usernameChanged, + user, + name, + previousName, + rawName, + nameChanged, +}: { + username: string; + previousUsername: string; + rawUsername?: string; + usernameChanged: boolean; + user: IUser; + name: string; + previousName: string | undefined; + rawName?: string; + nameChanged: boolean; +}): Promise { + if (usernameChanged && typeof rawUsername !== 'undefined') { + const fileStore = FileUpload.getStore('Avatars'); + const previousFile = await fileStore.model.findOneByName(previousUsername); + const file = await fileStore.model.findOneByName(username); + if (file) { + await fileStore.model.deleteFile(file._id); + } + if (previousFile) { + await fileStore.model.updateFileNameById(previousFile._id, username); + } - // update name and fname of group direct messages - await updateGroupDMsName(user); + await Messages.updateAllUsernamesByUserId(user._id, username); + await Messages.updateUsernameOfEditByUserId(user._id, username); - // update name and username of users on video conferences - await VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName); + const cursor = Messages.findByMention(previousUsername); + for await (const msg of cursor) { + const updatedMsg = msg.msg.replace(new RegExp(`@${previousUsername}`, 'ig'), `@${username}`); + await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); } + + await Rooms.replaceUsername(previousUsername, username); + await Rooms.replaceMutedUsername(previousUsername, username); + await Rooms.replaceUsernameOfUserByUserId(user._id, username); + await Subscriptions.setUserUsernameByUserId(user._id, username); + + await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); } - return true; + // update other references if either the name or username has changed + if (usernameChanged || nameChanged) { + // update name and fname of 1-on-1 direct messages + await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); + + // update name and fname of group direct messages + await updateGroupDMsName(user); + + // update name and username of users on video conferences + await VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName); + } } diff --git a/apps/meteor/app/lib/server/functions/sendMessage.js b/apps/meteor/app/lib/server/functions/sendMessage.js index a1399b5b19e94..72247d4b18703 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 05c17906374e5..9c544bd9a3336 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 597d03752dccd..8fa779ec96445 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 0000000000000..bf8d2474b7a7a --- /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 57490d1712c16..0000000000000 --- 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 0000000000000..b40447ca56abd --- /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 17296d8f374a7..ce262e4e67565 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 ff8182cec8c9b..98cea517bed4b 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 65298949a345f..75097b5c89b88 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 3db0ee76e997c..0000000000000 --- 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 355fd49916ae0..0fa3ac0b3c3b4 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/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 6540b67d79aaf..095baefaa2942 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -17,6 +17,7 @@ import { } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; import { Livechat } from '../../../server/lib/Livechat'; +import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; API.v1.addRoute( 'livechat/department', @@ -192,7 +193,7 @@ API.v1.addRoute( }, { async post() { - await Livechat.archiveDepartment(this.urlParams._id); + await LivechatTs.archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -207,11 +208,8 @@ API.v1.addRoute( }, { async post() { - if (await Livechat.unarchiveDepartment(this.urlParams._id)) { - return API.v1.success(); - } - - return API.v1.failure(); + await LivechatTs.unarchiveDepartment(this.urlParams._id); + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js index 6521c0f662dd8..7ecb3b3fc100c 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 a12d4a988281b..a7660827b0ce1 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 dbb6f8a6633a1..4cbafcb0dc737 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 e559aecc892e9..0abed5197d787 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/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index b13c48bb579da..7a7b70ea7c8b9 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -73,16 +73,21 @@ API.v1.addRoute( const agentId = inputAgentId || this.userId; - const agent = await Users.findOneAgentById>(agentId, { + const agent = await Users.findOneAgentById>(agentId, { projection: { status: 1, statusLivechat: 1, + active: 1, }, }); if (!agent) { return API.v1.notFound('Agent not found'); } + if (!agent.active) { + return API.v1.failure('error-user-deactivated'); + } + const newStatus: ILivechatAgentStatus = status || (agent.statusLivechat === ILivechatAgentStatus.AVAILABLE ? ILivechatAgentStatus.NOT_AVAILABLE : ILivechatAgentStatus.AVAILABLE); diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 517acf33f137b..57c1d117f1b02 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 2b6f4c00af53a..104e2ece94d50 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 0fe60248bfbae..8f61517974631 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -18,7 +18,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { isWidget } from '../../../../api/server/helpers/isWidget'; -import { canAccessRoomAsync } from '../../../../authorization/server'; +import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { settings as rcSettings } from '../../../../settings/server'; @@ -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) { @@ -352,7 +352,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.visitor', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTLivechatRoomVisitorParams, deprecationVersion: '7.0.0' }, + { + authRequired: true, + permissionsRequired: ['change-livechat-room-visitor'], + validateParams: isPUTLivechatRoomVisitorParams, + deprecationVersion: '7.0.0', + }, { async put() { // This endpoint is deprecated and will be removed in future versions. @@ -363,7 +368,7 @@ API.v1.addRoute( throw new Error('invalid-visitor'); } - const room = await LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + const room = await LivechatRooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, v: 1 } }); // TODO: check _id if (!room) { throw new Error('invalid-room'); } @@ -373,7 +378,7 @@ API.v1.addRoute( throw new Error('invalid-room-visitor'); } - const roomAfterChange = await Livechat.changeRoomVisitor(this.userId, rid, visitor); + const roomAfterChange = await LivechatTyped.changeRoomVisitor(this.userId, room, visitor); if (!roomAfterChange) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 5ce0ddc4ca379..52cd8738bec99 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,7 +6,7 @@ import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; API.v1.addRoute( diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 012b412639eab..84f7b96e155d3 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'); } @@ -174,7 +174,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await Livechat.updateCallStatus(callId, rid, callStatus, guest); + await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/webhooks.ts b/apps/meteor/app/livechat/server/api/v1/webhooks.ts index dceb19ed0420a..e282e2bd548ba 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/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 2c5ffe38169db..55de5bbf63150 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,13 +1,10 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; -import { businessHourLogger } from '../lib/logger'; -import { filterBusinessHoursThatMustBeOpened } from './Helper'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -61,29 +58,6 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true }, ); } - - async onNewAgentCreated(agentId: string): Promise { - businessHourLogger.debug(`Executing onNewAgentCreated for agentId: ${agentId}`); - - const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); - if (!defaultBusinessHour) { - businessHourLogger.debug(`No default business hour found for agentId: ${agentId}`); - return; - } - - const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); - if (!businessHourToOpen.length) { - businessHourLogger.debug( - `No business hour to open found for agentId: ${agentId}. Default business hour is closed. Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.NOT_AVAILABLE}`, - ); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); - return; - } - - await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); - - businessHourLogger.debug(`Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.AVAILABLE}`); - } } export abstract class AbstractBusinessHourType { diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index 52ccd0441e24e..c541e5f7b2c36 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 e61bb1621765c..e96ccb4c7b89a 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 63135fa532247..5d2730dba9a14 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,13 +1,13 @@ -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { - businessHourLogger.debug('opening single business hour'); return openBusinessHourDefault(); } @@ -22,10 +22,38 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp } async onStartBusinessHours(): Promise { - businessHourLogger.debug('Starting Single Business Hours'); return openBusinessHourDefault(); } + async onNewAgentCreated(agentId: string): Promise { + const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + businessHourLogger.debug('No default business hour found for agentId', { + agentId, + }); + return; + } + + const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); + if (!businessHourToOpen.length) { + businessHourLogger.debug({ + msg: 'No business hours found. Moving agent to NOT_AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.NOT_AVAILABLE, + }); + await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + return; + } + + await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); + + businessHourLogger.debug({ + msg: 'Business hours found. Moving agent to AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.AVAILABLE, + }); + } + afterSaveBusinessHours(): Promise { return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 50fe5846637b2..0419f1d02a1d9 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -1,8 +1,8 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import { type IUser } from '@rocket.chat/core-typings'; +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; @@ -33,14 +33,12 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug('Removing agent', user._id); - await Livechat.removeAgent(user.username); + 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 5467e517768e0..ad68fcf5ce5c4 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -1,5 +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'; @@ -15,28 +17,52 @@ callbacks.add( return message; } + // skips this callback if the message is a system message + if (message.t) { + return message; + } + // if the message has a token, it was sent by the visitor, so ignore it if (message.token) { 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 - if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse)) { + // check if room is yet awaiting for response from visitor + if (!room.waitingResponse) { + // case where agent sends second message or any subsequent message in a room before visitor responds to the first message + // in this case, we just need to update the lastMessageTs of the responseBy object + if (room.responseBy) { + await LivechatRooms.setAgentLastMessageTs(room._id); + } return message; } - await LivechatRooms.setResponseByRoomId(room._id, { - user: { - _id: message.u._id, - username: message.u.username, - }, - }); + // This is the first message from agent after visitor had last responded + const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || { + _id: message.u._id, + username: message.u.username, + firstResponseTs: new Date(message.ts), + lastMessageTs: new Date(message.ts), + }; + + // this unsets waitingResponse and sets responseBy object + await LivechatRooms.setResponseByRoomId(room._id, responseBy); return message; }, - callbacks.priority.LOW, + callbacks.priority.HIGH, 'markRoomResponded', ); diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index ec584ec001d67..e92e6b4d940b3 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/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index 7b4d9b89f14c2..6f42a910417d6 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; callbacks.add( 'livechat.newRoom', diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 5f6e3469501e2..28bed221afbf0 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 0dd48a328fd13..f17015e52e795 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 52138740e2952..e1d6626c7ddba 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -4,7 +4,7 @@ import dns from 'dns'; import util from 'util'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, @@ -30,7 +30,6 @@ import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; @@ -285,7 +284,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 +297,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 +461,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 +603,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 +626,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); @@ -722,40 +717,6 @@ export const Livechat = { return ret; }, - async unarchiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - // TODO: these kind of actions should be on events instead of here - await LivechatDepartmentAgents.enableAgentsByDepartmentId(_id); - return LivechatDepartmentRaw.unarchiveDepartment(_id); - }, - - async archiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - await LivechatDepartmentAgents.disableAgentsByDepartmentId(_id); - await LivechatDepartmentRaw.archiveDepartment(_id); - - this.logger.debug({ msg: 'Running livechat.afterDepartmentArchived callback for department:', departmentId: _id }); - await callbacks.run('livechat.afterDepartmentArchived', department); - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -771,28 +732,6 @@ export const Livechat = { }); }, - async getRoomMessages({ rid }) { - check(rid, String); - - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }).toArray(); - }, - async requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); @@ -896,20 +835,6 @@ export const Livechat = { }); }, - async notifyAgentStatusChanged(userId, status) { - callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); - if (!settings.get('Livechat_show_agent_info')) { - return; - } - - await LivechatRooms.findOpenByAgent(userId).forEach((room) => { - void api.broadcast('omnichannel.room', room._id, { - type: 'agentStatus', - status, - }); - }); - }, - async allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; @@ -917,56 +842,4 @@ export const Livechat = { return businessHourManager.allowAgentChangeServiceStatus(agentId); }, - - notifyRoomVisitorChange(roomId, visitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - }, - - async changeRoomVisitor(userId, roomId, visitor) { - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await hasPermissionAsync(userId, 'change-livechat-room-visitor'))) { - throw new Error('error-not-authorized'); - } - - const room = await LivechatRooms.findOneById(roomId, { ...roomAccessAttributes, _id: 1, t: 1 }); - - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - Livechat.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(roomId); - }, - async updateLastChat(contactId, lastChat) { - const updateUser = { - $set: { - lastChat, - }, - }; - await LivechatVisitors.updateById(contactId, updateUser); - }, - async updateCallStatus(callId, rid, status, user) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user); - } - }, }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 1c60a257d3192..afb6494883005 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, @@ -23,6 +23,7 @@ import { Users, LivechatDepartmentAgents, ReadReceipts, + Rooms, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -32,8 +33,10 @@ import type { FindCursor, UpdateFilter } from 'mongodb'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -305,7 +308,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; } @@ -808,6 +811,112 @@ class LivechatClass { return true; } + + async updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { + await Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + if (await VideoConf.declineLivechatCall(callId)) { + return; + } + + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); + } + } + + async updateLastChat(contactId: string, lastChat: Required) { + const updateUser = { + $set: { + lastChat, + }, + }; + await LivechatVisitors.updateById(contactId, updateUser); + } + + notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) { + void api.broadcast('omnichannel.room', roomId, { + type: 'visitorData', + visitor, + }); + } + + async changeRoomVisitor(userId: string, room: IOmnichannelRoom, visitor: ILivechatVisitor) { + const user = await Users.findOneById(userId, { projection: { _id: 1 } }); + if (!user) { + throw new Error('error-user-not-found'); + } + + if (!(await canAccessRoomAsync(room, user))) { + throw new Error('error-not-allowed'); + } + + await LivechatRooms.changeVisitorByRoomId(room._id, visitor); + + this.notifyRoomVisitorChange(room._id, visitor); + + return LivechatRooms.findOneById(room._id); + } + + async notifyAgentStatusChanged(userId: string, status?: UserStatus) { + if (!status) { + return; + } + + void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); + if (!settings.get('Livechat_show_agent_info')) { + return; + } + + await LivechatRooms.findOpenByAgent(userId).forEach((room) => { + void api.broadcast('omnichannel.room', room._id, { + type: 'agentStatus', + status, + }); + }); + } + + async getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }).toArray(); + } + + async archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + await callbacks.run('livechat.afterDepartmentArchived', department); + } + + async unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + return true; + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 597f38b71ec07..aed0061e808ef 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 0e975ca067636..f2fd7010eb12e 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 94fae239b74c2..9cd5de75a0f38 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 48313f1ce67c0..76b7f276d6719 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 c403dcd3edac4..69f3a4a2d80c8 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 2dc796fc6c94a..3817b10bf42b5 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 ea220b24d1491..2557fcdeb83d0 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 f24f88975b227..f9fce509e39ad 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 new file mode 100644 index 0000000000000..b1355c90cb93d --- /dev/null +++ b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts @@ -0,0 +1,35 @@ +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[]; + otherMentions: any[]; + message: IMessage; +} + +const beforeGetMentions = async (mentionIds: string[], extra?: IExtraDataForNotification) => { + const { otherMentions } = extra ?? {}; + + const teamIds = otherMentions?.filter(({ type }) => type === 'team').map(({ _id }) => _id); + + if (!teamIds?.length) { + return mentionIds; + } + + const members = await Team.getMembersByTeamIds(teamIds, { projection: { userId: 1 } }); + 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/index.ts b/apps/meteor/app/mentions/server/index.ts index 474d41a439e1c..a04af05b9db15 100644 --- a/apps/meteor/app/mentions/server/index.ts +++ b/apps/meteor/app/mentions/server/index.ts @@ -1,2 +1,3 @@ -import './server'; +import './getMentionedTeamMembers'; import './methods/getUserMentionsByChannel'; +import './server'; diff --git a/apps/meteor/app/mentions/server/server.ts b/apps/meteor/app/mentions/server/server.ts index 5eb70aae4656f..13765e99d8560 100644 --- a/apps/meteor/app/mentions/server/server.ts +++ b/apps/meteor/app/mentions/server/server.ts @@ -1,5 +1,5 @@ -import { api } from '@rocket.chat/core-services'; -import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import { api, Team } from '@rocket.chat/core-services'; +import type { IUser, IRoom, ITeam } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -9,16 +9,32 @@ import { settings } from '../../settings/server'; import MentionsServer from './Mentions'; export class MentionQueries { - async getUsers(usernames: string[]): Promise<(Pick & { type: 'user' })[]> { + async getUsers( + usernames: string[], + ): Promise<((Pick & { type: 'user' }) | (Pick & { type: 'team' }))[]> { + const uniqueUsernames = [...new Set(usernames)]; + const teams = await Team.listByNames(uniqueUsernames, { projection: { name: 1 } }); + const users = await Users.find( - { username: { $in: [...new Set(usernames)] } }, + { username: { $in: uniqueUsernames } }, { projection: { _id: true, username: true, name: 1 } }, ).toArray(); - return users.map((user) => ({ + const taggedUsers = users.map((user) => ({ ...user, - type: 'user', + type: 'user' as const, })); + + if (settings.get('Troubleshoot_Disable_Teams_Mention')) { + return taggedUsers; + } + + const taggedTeams = teams.map((team) => ({ + ...team, + type: 'team' as const, + })); + + return [...taggedUsers, ...taggedTeams]; } async getUser(userId: string): Promise { diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index a36883abb0a5d..906f0c98c1812 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 06c3014a8a56b..f62ab71f23026 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 e80e456c679bc..d2d0f85d19ce2 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 256722cdd3d4a..79de0402043f5 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 a3c70f012fa1e..104d50c569260 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 9917775aca069..5376bd6ae64b8 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 dfe27d8d5dc4d..33d0278f81a3a 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/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index b77a268fe1144..b3aa683371066 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -46,7 +46,7 @@ export class SAUMonitorClass { constructor() { this._started = false; this._dailyComputeJobName = 'aggregate-sessions'; - this._dailyFinishSessionsJobName = 'aggregate-sessions'; + this._dailyFinishSessionsJobName = 'finish-sessions'; } async start(): Promise { @@ -318,33 +318,19 @@ export class SAUMonitorClass { return; } - logger.info('[aggregate] - Aggregating data.'); + const today = new Date(); - const date = new Date(); - date.setDate(date.getDate() - 0); // yesterday - const yesterday = getDateObj(date); + // get sessions from 3 days ago to make sure even if a few cron jobs were skipped, we still have the data + const threeDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 3, 0, 0, 0, 0); - for await (const record of aggregates.dailySessionsOfYesterday(Sessions.col, yesterday)) { - await Sessions.updateOne( - { _id: `${record.userId}-${record.year}-${record.month}-${record.day}` }, - { $set: record }, - { upsert: true }, - ); + const period = { start: getDateObj(threeDaysAgo), end: getDateObj(today) }; + + logger.info({ msg: '[aggregate] - Aggregating data.', period }); + + for await (const record of aggregates.dailySessions(Sessions.col, period)) { + await Sessions.updateDailySessionById(`${record.userId}-${record.year}-${record.month}-${record.day}`, record); } - await Sessions.updateMany( - { - type: 'session', - year: { $lte: yesterday.year }, - month: { $lte: yesterday.month }, - day: { $lte: yesterday.day }, - }, - { - $set: { - type: 'computed-session', - _computedAt: new Date(), - }, - }, - ); + await Sessions.updateAllSessionsByDateToComputed(period); } } diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8cfe45b422327..64543deb88a11 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 311327fb6f734..d1f4b8d11fb64 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 78112bcee343f..1e00a0e47433f 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 4609797e6bd2a..a926f8540d27e 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 5618442ee6da4..5807673188e53 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 91ef0989bd190..040b6dfa9dc28 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 a388548c18a80..adb4c2ab1ae99 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 a5d2fcb9bc571..b9e235456291c 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 ac0c0e443453c..d17191a09be70 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 dcf82342917c8..0374254a7de96 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 50d53da351bc2..38aadf0a840ba 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 0a2717a65552c..6036f14049a41 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 0000000000000..db4c33654a926 --- /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 0000000000000..77718de0f441c --- /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/GazzodownText.tsx b/apps/meteor/client/components/GazzodownText.tsx index 7912e44ad9258..76868f84e3af9 100644 --- a/apps/meteor/client/components/GazzodownText.tsx +++ b/apps/meteor/client/components/GazzodownText.tsx @@ -49,6 +49,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe const useEmoji = Boolean(useUserPreference('useEmojis')); const useRealName = Boolean(useSetting('UI_Use_Real_Name')); const ownUserId = useUserId(); + const showMentionSymbol = Boolean(useUserPreference('mentionsWithSymbol')); const chat = useChat(); @@ -122,6 +123,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe useRealName, isMobile, ownUserId, + showMentionSymbol, }} > {children} diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx index e02d6dc4e7463..f660b4b85f35a 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 ec9f852c8a8be..513b31dd81fae 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 39564ca7f89f3..88f5f1a5c6e71 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 3b39ea79d42f8..fd3c0a29effe8 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 4bd85be403426..ce5704b664829 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 17cbc094160bc..67d650186680a 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 82c92d39cc8f2..bdbde6b05acd2 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 e879caf38032a..2757b5d9a88ba 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 7026c6cab35f7..eb0414e7da144 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 1b7936cafd3e2..19074f7f6b340 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 3d604fc5004b4..4c91e274de68c 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 266e925869444..04aa7a4f94f05 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 96d11cf44cb76..f96c198865cce 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 dfda12cdd2e49..857af5e9c43f0 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/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 32948eb422433..04b07e9cd627d 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -71,7 +71,7 @@ const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar } danger icon='trash' title={t('Accounts_SetDefaultAvatar')} - disabled={roomAvatar === null || isRoomFederated(room) || disabled} + disabled={!roomAvatar || isRoomFederated(room) || disabled} onClick={clickReset} /> diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index 4674528a483fa..5552e6da07452 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/components/message/toolbox/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx index 4c5d442652f21..54a320ebf3d79 100644 --- a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx @@ -101,6 +101,7 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility, ...props }: Messag data-qa-type='message-action' data-qa-id={option.id} role={option.role ? option.role : 'button'} + gap={!option.icon && option.type === 'apps'} /> ))} {index !== arr.length - 1 && } diff --git a/apps/meteor/client/definitions/info.d.ts b/apps/meteor/client/definitions/info.d.ts index 2b66032f484af..43fa1fc534148 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/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index e1c3126985ae8..73b0f34836e17 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -5,13 +5,15 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; -import { useRoom } from '../../views/room/contexts/RoomContext'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useReactiveValue } from '../useReactiveValue'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); const room = useRoom(); + const subscription = useRoomSubscription(); const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted; const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id); const permittedToEditRoom = usePermission('edit-room', room._id); @@ -21,8 +23,22 @@ export const useE2EERoomAction = () => { const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); - const action = useMutableCallback(() => { - void toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + const action = useMutableCallback(async () => { + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: room.encrypted + ? t('E2E_Encryption_disabled_for_room', { roomName: room.name }) + : t('E2E_Encryption_enabled_for_room', { roomName: room.name }), + }); + + if (subscription?.autoTranslate) { + dispatchToastMessage({ type: 'success', message: t('AutoTranslate_Disabled_for_room', { roomName: room.name }) }); + } }); const enabledOnRoom = !!room.encrypted; diff --git a/apps/meteor/client/hooks/useAppTranslations.ts b/apps/meteor/client/hooks/useAppTranslations.ts index ad8ca5966c2bc..bf4f83e48d851 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 99b7e5e3461c1..0f568d9bd5cc8 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 8b091459291b5..9260d672bec58 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 2b661ae89cf36..724f61d8c7c87 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 e5ff73be77b19..95f7f1dcb3618 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/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 1630071be6580..8242de07d7917 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -144,7 +144,7 @@ export type ChatAPI = { ActionManager: any; readonly flows: { - readonly uploadFiles: (files: readonly File[]) => Promise; + readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index bd6e014588630..f2c049ad04b14 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/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 58eb18400a30e..1411ad5a004ed 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -6,7 +6,7 @@ import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; -export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promise => { +export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; const msg = await prependReplies('', replies); @@ -52,4 +52,5 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promis }; uploadNextFile(); + resetFileInput?.(); }; diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index bb08f0242d4f5..3345159807874 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 46f5bcb8d68de..f07d828a46021 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 d59198ebd1d3f..0000000000000 --- 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 0dd7ee31deed8..0f146ec83128f 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 5850f0da3982e..2cf47066c4e4f 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); } @@ -226,6 +231,7 @@ const TranslationProvider = ({ children }: TranslationProviderProps): ReactEleme useEffect(() => { if (moment.locales().includes(language.toLowerCase())) { + moment.locale(language); return; } diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index fa9d683815ed2..432a197671f32 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -71,7 +71,8 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); - useCreateFontStyleElement(user?.settings?.preferences?.fontSize); + const createFontStyleElement = useCreateFontStyleElement(); + createFontStyleElement(user?.settings?.preferences?.fontSize); const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; diff --git a/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx index 69afd3c2667ac..593bd784be90c 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/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index b96c54d4c955d..f275ff2800d82 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -33,6 +33,30 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnTyp return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`; }; +const getBadgeTitle = ( + userMentions: number, + threadUnread: number, + groupMentions: number, + unread: number, + t: ReturnType, +) => { + const title = [] as string[]; + if (userMentions) { + title.push(t('mentions_counter', { count: userMentions })); + } + if (threadUnread) { + title.push(t('threads_counter', { count: threadUnread })); + } + if (groupMentions) { + title.push(t('group_mentions_counter', { count: groupMentions })); + } + const count = unread - userMentions - groupMentions; + if (count > 0) { + title.push(t('unread_messages_counter', { count })); + } + return title.join(', '); +}; + type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -137,10 +161,12 @@ function SideBarItemTemplateWithData({ const isUnread = unread > 0 || threadUnread; const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0)); + const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t); + const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} diff --git a/apps/meteor/client/sidebar/Sidebar.tsx b/apps/meteor/client/sidebar/Sidebar.tsx index ae333bbdb2a1d..84c63eac01bea 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 57ebffdbcf39d..3f001b158d728 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 1f12c2b59b495..e14ef6e77b370 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 a0f90a7d2f2fb..6ea69ce6c0371 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 9fcc7a0d22749..a53836eda311d 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 248b914187394..b0b20972d346f 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 20006cf015887..646b85c838be4 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 b9432f821373b..56041ab4e571c 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 3e27dd22c7fac..ca2855d09db59 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 436c7c1dc71d2..fa5dfd2797cb1 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 542fa05c54ab9..e7dec5f3506a5 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 45f98634a3731..4e42874eba4a3 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 440b55ce5e6d2..6b2b66ec69d79 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 f289cf2eeee2d..657548d5a1b9c 100644 --- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx +++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx @@ -1,59 +1,95 @@ -import { Accordion, Box, Button, ButtonGroup, Field, FieldGroup, RadioButton, Select, Tag } from '@rocket.chat/fuselage'; +import { css } from '@rocket.chat/css-in-js'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Icon, + FieldDescription, + Accordion, + Box, + Button, + ButtonGroup, + Field, + FieldGroup, + FieldHint, + FieldLabel, + FieldRow, + RadioButton, + Select, + Tag, + ToggleSwitch, +} from '@rocket.chat/fuselage'; import { useLocalStorage, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import type { FontSize } from '@rocket.chat/rest-typings'; -import { useSetModal, useTranslation, useToastMessageDispatch, useUserPreference, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes'; -import React from 'react'; +import { useSetModal, useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React, { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Page from '../../../components/Page'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; +import { getDirtyFields } from '../../../lib/getDirtyFields'; import HighContrastUpsellModal from './HighContrastUpsellModal'; +import MentionsWithSymbolUpsellModal from './MentionsWithSymbolUpsellModal'; import { fontSizes } from './fontSizes'; -import { useAdjustableFontSize } from './hooks/useAdsjustableFontSize'; +import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues'; +import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues'; +import { useCreateFontStyleElement } from './hooks/useCreateFontStyleElement'; import { themeItems as themes } from './themeItems'; const AccessibilityPage = () => { const t = useTranslation(); const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); + const preferencesValues = useAccessiblityPreferencesValues(); const { data: license } = useIsEnterprise(); + const isEnterprise = license?.isEnterprise; - const fontSizeId = useUniqueId(); - const [fontSize, setFontSize] = useAdjustableFontSize(); + const { themeAppearence } = preferencesValues; + const [, setPrevTheme] = useLocalStorage('prevTheme', themeAppearence); + const createFontStyleElement = useCreateFontStyleElement(); + const displayRolesEnabled = useSetting('UI_DisplayRoles'); - const themePreference = useUserPreference('themeAppearence') || 'auto'; - const [, setPrevTheme] = useLocalStorage('prevTheme', themePreference); + const timeFormatOptions = useMemo( + (): SelectOption[] => [ + ['0', t('Default')], + ['1', t('12_Hour')], + ['2', t('24_Hour')], + ], + [t], + ); - const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); + const fontSizeId = useUniqueId(); + const mentionsWithSymbolId = useUniqueId(); + const clockModeId = useUniqueId(); + const hideUsernamesId = useUniqueId(); + const hideRolesId = useUniqueId(); const { formState: { isDirty, dirtyFields }, handleSubmit, control, reset, + watch, } = useForm({ - defaultValues: { themeAppearence: themePreference, fontSize }, + defaultValues: preferencesValues, + }); + + const currentData = watch(); + + const setUserPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); + + const setPreferencesAction = useMutation({ + mutationFn: setUserPreferencesEndpoint, + onSuccess: () => dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }), + onError: (error) => dispatchToastMessage({ type: 'error', message: error }), + onSettled: (_data, _error, { data: { fontSize } }) => { + reset(currentData); + dirtyFields.themeAppearence && setPrevTheme(themeAppearence); + dirtyFields.fontSize && fontSize && createFontStyleElement(fontSize); + }, }); - const handleSave = async ({ themeAppearence, fontSize }: { themeAppearence: ThemePreference; fontSize: FontSize }) => { - try { - await setUserPreferences({ data: { themeAppearence, fontSize } }); - // dirtyFields.themeAppearence && (await setUserPreferences({ data: { themeAppearence, fontSize } })); - // dirtyFields.fontSize && (await setUserPreferences({ data: { fontSize } })); - dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - if (dirtyFields.themeAppearence) { - setPrevTheme(themePreference); - } - if (dirtyFields.fontSize) { - setFontSize(fontSize); - } - reset({ themeAppearence, fontSize }); - } + const handleSaveData = (formData: AccessibilityPreferencesData) => { + const data = getDirtyFields(formData, dirtyFields); + setPreferencesAction.mutateAsync({ data }); }; return ( @@ -67,25 +103,28 @@ 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 && ( - {t('Enterprise')} + + + {t('Enterprise')} + )} - - + + { - if (communityDisabled) { + if (showCommunityUpsellTriggers) { return ( { return onChange(id)} checked={value === id} />; }} /> - + - + {t.has(description) ? t(description) : description} - + ); })} @@ -110,10 +149,10 @@ const AccessibilityPage = () => { - + {t('Font_size')} - - + + { + )} + /> + + + + + {t('Show_usernames')} + + ( + onChange(!(e.target as HTMLInputElement).checked)} + /> + )} + /> + + + {t('Show_or_hide_the_username_of_message_authors')} + + {displayRolesEnabled && ( + + + {t('Show_roles')} + + ( + onChange(!(e.target as HTMLInputElement).checked)} + /> + )} + /> + + + {t('Show_or_hide_the_user_roles_of_message_authors')} + + )} @@ -131,8 +263,8 @@ const AccessibilityPage = () => { - - + diff --git a/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx index c06501aae282d..bcb28ea587d12 100644 --- a/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx +++ b/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx @@ -33,7 +33,7 @@ const HighContrastUpsellModal = ({ onClose }: { onClose: () => void }) => { onClose={onClose} onCancel={handleTalkToSales} onConfirm={handleGoFullyFeatured} - cancelText={t('Talk_to_sales')} + cancelText={t('Talk_to_an_expert')} confirmText={t('Start_free_trial')} /> ); diff --git a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx new file mode 100644 index 0000000000000..b92ca74d0f6e6 --- /dev/null +++ b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx @@ -0,0 +1,40 @@ +import { useRole, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import GenericUpsellModal from '../../../components/GenericUpsellModal'; +import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; + +const MentionsWithSymbolUpsellModal = ({ onClose }: { onClose: () => void }) => { + const t = useTranslation(); + + const isAdmin = useRole('admin'); + const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions(); + + if (!isAdmin) { + return ( + + ); + } + return ( + + ); +}; +export default MentionsWithSymbolUpsellModal; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts b/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts new file mode 100644 index 0000000000000..339cdcff7dd9b --- /dev/null +++ b/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts @@ -0,0 +1,31 @@ +import type { FontSize } from '@rocket.chat/rest-typings'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes'; + +export type AccessibilityPreferencesData = { + themeAppearence?: ThemePreference; + fontSize?: FontSize; + fontSizePreference?: FontSize; + mentionsWithSymbol?: boolean; + clockMode?: 0 | 1 | 2; + hideUsernames?: boolean; + hideRoles?: boolean; +}; + +export const useAccessiblityPreferencesValues = (): AccessibilityPreferencesData => { + const themeAppearence = useUserPreference('themeAppearence') || 'auto'; + const fontSize = useUserPreference('fontSize') || '100%'; + const mentionsWithSymbol = useUserPreference('mentionsWithSymbol') || false; + const clockMode = useUserPreference<0 | 1 | 2>('clockMode') ?? 0; + const hideUsernames = useUserPreference('hideUsernames'); + const hideRoles = useUserPreference('hideRoles'); + + return { + themeAppearence, + fontSize, + mentionsWithSymbol, + clockMode, + hideUsernames, + hideRoles, + }; +}; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx b/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx deleted file mode 100644 index 35607de1f9b58..0000000000000 --- a/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { FontSize } from '@rocket.chat/rest-typings'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useState } from 'react'; - -import { useCreateFontStyleElement } from './useCreateFontStyleElement'; - -export const useAdjustableFontSize = (): [FontSize, (value: FontSize) => void] => { - const fontSizePreference = useUserPreference('fontSize') || '100%'; - const [fontSize, setFontSize] = useState(fontSizePreference); - - useCreateFontStyleElement(fontSize); - - return [fontSize, setFontSize]; -}; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts b/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts index 63ecd3c0cf4b1..923b3af88c602 100644 --- a/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts +++ b/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useMemo } from 'react'; const createStyleElement = (id: string) => { const styleElement = document.getElementById(id); @@ -10,11 +10,14 @@ const createStyleElement = (id: string) => { return newStyleElement; }; -export const useCreateFontStyleElement = (fontSize: string): void => { - useEffect(() => { - const styleElement = createStyleElement('rcx-font-size'); - const css = `html { font-size: ${fontSize}; }`; - styleElement.innerHTML = css; - document.head.appendChild(styleElement); - }, [fontSize]); +export const useCreateFontStyleElement = (): ((fontSize: string) => void) => { + return useMemo( + () => (fontSize: string) => { + const styleElement = createStyleElement('rcx-font-size'); + const css = `html { font-size: ${fontSize}; }`; + styleElement.innerHTML = css; + document.head.appendChild(styleElement); + }, + [], + ); }; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index 715f6fb9b1256..29b2a796953e8 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -3,13 +3,16 @@ import { ButtonGroup, Button, Box, - Field, ToggleSwitch, - FieldGroup, States, StatesIcon, StatesTitle, Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, } from '@rocket.chat/fuselage'; import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; import { useFeaturePreviewList } from '@rocket.chat/ui-client'; @@ -104,12 +107,12 @@ const AccountFeaturePreviewPage = () => { - {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 71d025d792cd4..54806d879aa11 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 fb1903806fec6..9fccaef4593e3 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 4e616072d1850..d09492fdc5a67 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 8c05a92bad47d..85bfc9072b127 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 2faaa9da89c24..0dc6e25f1ac33 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('Message_TimeFormat')} + {t('Go_to_accessibility_and_appearance')} - {t('Use_Emojis')} - + {t('Use_Emojis')} + { )} /> - + - {t('Convert_Ascii_Emojis')} - + {t('Convert_Ascii_Emojis')} + { )} /> - + - {t('Auto_Load_Images')} - + {t('Auto_Load_Images')} + { )} /> - + - {t('Save_Mobile_Bandwidth')} - + {t('Save_Mobile_Bandwidth')} + { )} /> - + - {t('Collapse_Embedded_Media_By_Default')} - + {t('Collapse_Embedded_Media_By_Default')} + { )} /> - + - - {t('Hide_usernames')} - - ( - - )} - /> - - + {t('Hide_usernames')} + {t('Go_to_accessibility_and_appearance')} + + + {t('Hide_roles')} + {t('Go_to_accessibility_and_appearance')} - {displayRolesEnabled && ( - - - {t('Hide_roles')} - - ( - - )} - /> - - - - )} - {t('Hide_flextab')} - + {t('Hide_flextab')} + { )} /> - + - {t('Display_avatars')} - + {t('Display_avatars')} + { )} /> - + - {t('Enter_Behaviour')} - + {t('Enter_Behaviour')} + { )} /> - + - {t('Notification_Push_Default_For')} - + {t('Notification_Push_Default_For')} + { } /> - - {errors?.statusType && {t('error-the-field-is-required', { field: t('Presence') })}} + + {errors?.statusType && {t('error-the-field-is-required', { field: t('Presence') })}} diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx index 0975282d0022e..d659e27e0186c 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx @@ -12,6 +12,10 @@ import { Margins, NumberInput, PasswordInput, + FieldLabel, + FieldRow, + FieldError, + FieldHint, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; @@ -169,7 +173,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac - + {t('Active')} )} /> - + - {t('Name')}* - + {t('Name')}* + - - {errors.name && {errors.name?.message}} + + {errors.name && {errors.name?.message}} - {t('Email')}* - + {t('Email')}* + - - {errors.email && {errors.email?.message}} + + {errors.email && {errors.email?.message}} - {t('Description')} - + {t('Description')} + - + - {t('Sender_Info')} - + {t('Sender_Info')} + - - {t('Will_Appear_In_From')} + + {t('Will_Appear_In_From')} - {t('Department')} - + {t('Department')} + } /> - - {t('Only_Members_Selected_Department_Can_View_Channel')} + + {t('Only_Members_Selected_Department_Can_View_Channel')} - {t('Server')}* - + {t('Server')}* + - - {errors.smtpServer && {errors.smtpServer?.message}} + + {errors.smtpServer && {errors.smtpServer?.message}} - {t('Port')}* - + {t('Port')}* + - - {errors.smtpPort && {errors.smtpPort?.message}} + + {errors.smtpPort && {errors.smtpPort?.message}} - {t('Username')}* - + {t('Username')}* + - - {errors.smtpUsername && {errors.smtpUsername?.message}} + + {errors.smtpUsername && {errors.smtpUsername?.message}} - {t('Password')}* - + {t('Password')}* + - - {errors.smtpPassword && {errors.smtpPassword?.message}} + + {errors.smtpPassword && {errors.smtpPassword?.message}} - + {t('Connect_SSL_TLS')} )} /> - + - {t('Server')}* - + {t('Server')}* + - - {errors.imapServer && {errors.imapServer?.message}} + + {errors.imapServer && {errors.imapServer?.message}} - {t('Port')}* - + {t('Port')}* + - - {errors.imapPort && {errors.imapPort?.message}} + + {errors.imapPort && {errors.imapPort?.message}} - {t('Username')}* - + {t('Username')}* + - - {errors.imapUsername && {errors.imapUsername?.message}} + + {errors.imapUsername && {errors.imapUsername?.message}} - {t('Password')}* - + {t('Password')}* + - - {errors.imapPassword && {errors.imapPassword?.message}} + + {errors.imapPassword && {errors.imapPassword?.message}} - {t('Max_Retry')}* - + {t('Max_Retry')}* + - - {errors.imapRetries && {errors.imapRetries?.message}} + + {errors.imapRetries && {errors.imapRetries?.message}} - + {t('Connect_SSL_TLS')} )} /> - + - + - - + + {inboxData?._id && ( @@ -370,7 +374,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac )} - + diff --git a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx index f232be15b0c7a..2710721e2bcbe 100644 --- a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx @@ -1,5 +1,5 @@ import type { ProgressStep } from '@rocket.chat/core-typings'; -import { Box, Margins, Throbber } from '@rocket.chat/fuselage'; +import { Box, Margins, ProgressBar, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useEndpoint, useTranslation, useStream, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -137,16 +137,14 @@ const ImportProgressPage = function ImportProgressPage() { useEffect(() => { return streamer('progress', (progress) => { // There shouldn't be any progress update sending only the rate at this point of the process - if ('rate' in progress) { - return; + if (!('rate' in progress)) { + handleProgressUpdated({ + key: progress.key, + step: progress.step, + completed: progress.count.completed, + total: progress.count.total, + }); } - - handleProgressUpdated({ - key: progress.key, - step: progress.step, - completed: progress.count.completed, - total: progress.count.total, - }); }); }, [handleProgressUpdated, streamer]); @@ -167,11 +165,9 @@ const ImportProgressPage = function ImportProgressPage() { {t((progress.data.step[0].toUpperCase() + progress.data.step.slice(1)) as any)} - - - {progress.data.completed}/{progress.data.total} ( - {numberFormat((progress.data.completed / progress.data.total) * 100, 0)} - %) + + + {numberFormat((progress.data.completed / progress.data.total) * 100, 0)}% diff --git a/apps/meteor/client/views/admin/import/PrepareImportPage.js b/apps/meteor/client/views/admin/import/PrepareImportPage.js index 889646cf89869..3b86b3c06bf72 100644 --- a/apps/meteor/client/views/admin/import/PrepareImportPage.js +++ b/apps/meteor/client/views/admin/import/PrepareImportPage.js @@ -1,4 +1,4 @@ -import { Badge, Box, Button, ButtonGroup, Margins, Throbber, Tabs } from '@rocket.chat/fuselage'; +import { Badge, Box, Button, ButtonGroup, Margins, ProgressBar, Throbber, Tabs } from '@rocket.chat/fuselage'; import { useDebouncedValue, useSafely } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useTranslation, useStream, useRouter } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, useMemo } from 'react'; @@ -204,8 +204,10 @@ function PrepareImportPage() { <> {progressRate ? ( - - {numberFormat(progressRate, 0)}% + + + {numberFormat(progressRate, 0)}% + ) : ( diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index ebb92b040c833..41709d247f8b4 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -66,6 +66,8 @@ export default { _id: '', wizard: {}, uniqueId: '', + deploymentFingerprintHash: '', + deploymentFingerprintVerified: true, installedAt: '', version: '1.0.0', tag: '', @@ -170,6 +172,10 @@ export default { uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 }, + omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] }, apps: { engineVersion: 'x.y.z', enabled: false, @@ -265,6 +271,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.tsx index 1bf5accb489fd..7a95e7aae0180 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.tsx @@ -2,7 +2,7 @@ import type { IServerInfo, IStats, Serialized } from '@rocket.chat/core-typings' import { ButtonGroup, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { IInstance } from '@rocket.chat/rest-typings'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle, CardColSection, CardColTitle, CardFooter } from '@rocket.chat/ui-client'; import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; @@ -31,57 +31,58 @@ const DeploymentCard = ({ info, statistics, instances }: DeploymentCardProps): R return ( - {t('Deployment')} - - - - {t('Version')} + {t('Deployment')} + + + + {t('Version')} {statistics.version} - - - {t('Deployment_ID')} + + + {t('Deployment_ID')} {statistics.uniqueId} - + {appsEngineVersion && ( - - {t('Apps_Engine_Version')} + + {t('Apps_Engine_Version')} {appsEngineVersion} - + )} - - {t('Node_version')} + + {t('Node_version')} {statistics.process.nodeVersion} - - - {t('DB_Migration')} + + + {t('DB_Migration')} {`${statistics.migration.version} (${formatDateAndTime(statistics.migration.lockedAt)})`} - - - {t('MongoDB')} + + + {t('MongoDB')} {`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} ${ !statistics.msEnabled ? `(oplog ${statistics.oplogEnabled ? t('Enabled') : t('Disabled')})` : '' }`} - - - {t('Commit_details')} + + + {t('Commit_details')} {t('github_HEAD')}: ({commit.hash ? commit.hash.slice(0, 9) : ''})
- {t('Branch')}: {commit.branch} -
- - {t('PID')} + {t('Branch')}: {commit.branch}
+ {commit.subject} + + + {t('PID')} {statistics.process.pid} -
-
-
+ + + {!!instances.length && ( - + - + )}
); diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 222f31f883341..29c0c00d58140 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -96,6 +96,8 @@ export default { _id: '', wizard: {}, uniqueId: '', + deploymentFingerprintHash: '', + deploymentFingerprintVerified: true, installedAt: '', version: '', tag: '', @@ -200,6 +202,10 @@ export default { uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 }, + omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] }, apps: { engineVersion: 'x.y.z', enabled: false, @@ -295,6 +301,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/LicenseCard.tsx b/apps/meteor/client/views/admin/info/LicenseCard.tsx index 8865ed990a4ab..bccbddaa6db73 100644 --- a/apps/meteor/client/views/admin/info/LicenseCard.tsx +++ b/apps/meteor/client/views/admin/info/LicenseCard.tsx @@ -1,6 +1,6 @@ import { ButtonGroup, Button, Skeleton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle, CardColSection, CardColTitle, CardFooter } from '@rocket.chat/ui-client'; import { useSetModal, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -43,14 +43,14 @@ const LicenseCard = (): ReactElement => { return ( - {t('License')} - - - + {t('License')} + + + - - - {t('Features')} + + + {t('Features')} {isLoading ? ( <> @@ -67,10 +67,10 @@ const LicenseCard = (): ReactElement => { )} - - - - + + + + {isAirGapped ? ( )} - + ); }; diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index 14a6cac8633d6..a65e645b17d06 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -44,6 +44,8 @@ export default { _id: '', wizard: {}, uniqueId: '', + deploymentFingerprintHash: '', + deploymentFingerprintVerified: true, installedAt: '', version: '', tag: '', @@ -148,6 +150,10 @@ export default { uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 }, uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 }, + omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] }, + uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] }, apps: { engineVersion: 'x.y.z', enabled: false, @@ -243,6 +249,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/UsageCard.tsx b/apps/meteor/client/views/admin/info/UsageCard.tsx index 793789c30a03a..7a3b2123e5f27 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.tsx @@ -1,7 +1,17 @@ import type { IStats } from '@rocket.chat/core-typings'; import { ButtonGroup, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { TextSeparator, Card } from '@rocket.chat/ui-client'; +import { + Card, + CardBody, + CardCol, + CardTitle, + CardColSection, + CardColTitle, + CardFooter, + TextSeparator, + CardIcon, +} from '@rocket.chat/ui-client'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; @@ -29,15 +39,15 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { return ( - {t('Usage')} - - - - {t('Users')} + {t('Usage')} + + + + {t('Users')} - {t('Total')} + {t('Total')} } value={statistics.totalUsers} @@ -45,9 +55,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Online')} } @@ -56,9 +66,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Busy')} } @@ -67,9 +77,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Away')} } @@ -78,34 +88,34 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Offline')} } value={statistics.offlineUsers} /> - - - {t('Types_and_Distribution')} + + + {t('Types_and_Distribution')} - - - {t('Uploads')} + + + {t('Uploads')} - - - {t('Total_rooms')} + + + {t('Total_rooms')} - {t('Stats_Total_Rooms')} + {t('Stats_Total_Rooms')} } value={statistics.totalRooms} @@ -113,7 +123,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Channels')} + {t('Stats_Total_Channels')} } value={statistics.totalChannels} @@ -121,7 +131,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Private_Groups')} + {t('Stats_Total_Private_Groups')} } value={statistics.totalPrivateGroups} @@ -129,7 +139,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Direct_Messages')} + {t('Stats_Total_Direct_Messages')} } value={statistics.totalDirect} @@ -137,7 +147,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Total_Discussions')} + {t('Total_Discussions')} } value={statistics.totalDiscussions} @@ -145,30 +155,30 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Livechat_Rooms')} + {t('Stats_Total_Livechat_Rooms')} } value={statistics.totalLivechat} /> - - - {t('Total_messages')} + + + {t('Total_messages')} - - - - + + + + - + ); }; diff --git a/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js b/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js index 94bbd156b86cc..ae4d4fa411b5a 100644 --- a/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js +++ b/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js @@ -1,4 +1,4 @@ -import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins } from '@rocket.chat/fuselage'; +import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins, Select } from '@rocket.chat/fuselage'; import { useAbsoluteUrl, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useCallback } from 'react'; @@ -11,7 +11,8 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat const absoluteUrl = useAbsoluteUrl(); - const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script, overrideDestinationChannelEnabled } = formValues; + const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script, scriptEngine, overrideDestinationChannelEnabled } = + formValues; const { handleEnabled, @@ -24,6 +25,7 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat handleScriptEnabled, handleOverrideDestinationChannelEnabled, handleScript, + handleScriptEngine, } = formHandlers; const url = absoluteUrl(`hooks/${extraData._id}/${extraData.token}`); @@ -42,6 +44,14 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat url, }); + const scriptEngineOptions = useMemo( + () => [ + ['vm2', t('Script_Engine_vm2')], + ['isolated-vm', t('Script_Engine_isolated_vm')], + ], + [t], + ); + const hilightedExampleJson = useHighlightedCode('json', JSON.stringify(exampleData, null, 2)); return ( @@ -172,6 +182,18 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat ), [t, scriptEnabled, handleScriptEnabled], )} + {useMemo( + () => ( + + {t('Script_Engine')} + + + + {t('Script_Engine_Description')} + + ), + [scriptEngine, scriptEngineOptions, handleScriptEngine, t], + )} {useMemo( () => ( diff --git a/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js b/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js index cbe3c3e5377d9..e785f63ca29dd 100644 --- a/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js @@ -17,6 +17,7 @@ const getInitialValue = (data) => { avatar: data.avatar ?? '', emoji: data.emoji ?? '', scriptEnabled: data.scriptEnabled, + scriptEngine: data.scriptEngine ?? 'vm2', overrideDestinationChannelEnabled: data.overrideDestinationChannelEnabled, script: data.script, }; diff --git a/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js b/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js index 1734f32968c9f..383b9209519d2 100644 --- a/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js @@ -24,6 +24,7 @@ const getInitialValue = (data) => { avatar: data.avatar ?? '', emoji: data.emoji ?? '', scriptEnabled: data.scriptEnabled ?? false, + scriptEngine: data.scriptEngine ?? 'vm2', script: data.script ?? '', retryFailedCalls: data.retryFailedCalls ?? true, retryCount: data.retryCount ?? 5, diff --git a/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js b/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js index 019dc6d0d730a..7b4e0880e57ff 100644 --- a/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js @@ -15,6 +15,7 @@ const initialState = { avatar: '', emoji: '', scriptEnabled: false, + scriptEngine: 'isolated-vm', overrideDestinationChannelEnabled: false, script: '', }; diff --git a/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js b/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js index 818082f5f5dec..153dc4c6eb7fd 100644 --- a/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js @@ -23,6 +23,7 @@ const defaultData = { avatar: '', emoji: '', scriptEnabled: false, + scriptEngine: 'isolated-vm', script: '', retryFailedCalls: true, retryCount: 6, diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index 0f47d8ef17ce3..e917315957bb4 100644 --- a/apps/meteor/client/views/admin/mailer/MailerPage.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerPage.tsx @@ -1,4 +1,17 @@ -import { TextInput, TextAreaInput, Field, FieldGroup, CheckBox, Button, ButtonGroup, Box } from '@rocket.chat/fuselage'; +import { + TextInput, + TextAreaInput, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + CheckBox, + Button, + ButtonGroup, + Box, +} from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; @@ -63,10 +76,10 @@ const MailerPage = () => { - + {t('From')} - - + + { aria-invalid={errors.fromEmail ? 'true' : 'false'} aria-describedby={`${fromEmailId}-error`} /> - + {errors.fromEmail && ( - + {errors.fromEmail.message} - + )} - + { )} /> - {t('Dry_run')} - - {t('Dry_run_description')} + {t('Dry_run')} + + {t('Dry_run_description')} - {t('Query')} - + {t('Query')} + { aria-describedby={`${queryId}-error ${queryId}-hint`} aria-invalid={errors.query ? 'true' : 'false'} /> - + {errors.query && ( - + {errors.query.message} - + )} - {t('Query_description')} + {t('Query_description')} - + {t('Subject')} - - + + { aria-required='true' aria-invalid={errors.subject ? 'true' : 'false'} /> - + {errors.subject && ( - + {errors.subject.message} - + )} - + {t('Email_body')} - - + + { aria-required='true' aria-invalid={errors.emailBody ? 'true' : 'false'} /> - + {errors.emailBody && ( - + {errors.emailBody.message} - + )} - + diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx index 87bb1295d453e..4e877b3aefc71 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx @@ -1,4 +1,4 @@ -import { Pagination, Field } from '@rocket.chat/fuselage'; +import { Pagination, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; @@ -121,10 +121,10 @@ const ModerationConsoleTable: FC = () => { <> setText(text)} /> - {t('Date')} - + {t('Date')} + - + {isLoading && ( diff --git a/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx b/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx index f870dbc527763..326cc3382b4bc 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx @@ -10,7 +10,7 @@ type DateRangePickerProps = Omit, 'onChange'> & { onChange(range: { start: string; end: string }): void; }; -const formatToDateInput = (date: Moment) => date.format('YYYY-MM-DD'); +const formatToDateInput = (date: Moment) => date.locale('en').format('YYYY-MM-DD'); const todayDate = formatToDateInput(moment()); diff --git a/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx b/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx index a6eeea270eb65..a5759fde6cc7e 100644 --- a/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx @@ -1,5 +1,17 @@ import type { IOAuthApps, Serialized } from '@rocket.chat/core-typings'; -import { Button, ButtonGroup, TextInput, Field, TextAreaInput, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; +import { + Button, + ButtonGroup, + TextInput, + Field, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + TextAreaInput, + ToggleSwitch, + FieldGroup, +} from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useRoute, useAbsoluteUrl, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useCallback, useMemo } from 'react'; @@ -89,7 +101,7 @@ const EditOauthApp = ({ onChange, data, ...props }: EditOauthAppProps): ReactEle - + {t('Active')} } /> - + - {t('Application_Name')} - + {t('Application_Name')} + - - {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} - {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} + + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} + {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} - {t('Redirect_URI')} - + {t('Redirect_URI')} + - - {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} - {errors?.redirectUri && {t('error-the-field-is-required', { field: t('Redirect_URI') })}} + + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} + {errors?.redirectUri && {t('error-the-field-is-required', { field: t('Redirect_URI') })}} - {t('Client_ID')} - + {t('Client_ID')} + - + - {t('Client_Secret')} - + {t('Client_Secret')} + - + - {t('Authorization_URL')} - + {t('Authorization_URL')} + - + - {t('Access_Token_URL')} - + {t('Access_Token_URL')} + - + - + - + - + - + diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx index b663517676ff3..ccb4205b58196 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx @@ -1,4 +1,16 @@ -import { Button, ButtonGroup, TextInput, Field, TextAreaInput, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; +import { + Button, + ButtonGroup, + TextInput, + Field, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + TextAreaInput, + ToggleSwitch, + FieldGroup, +} from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; @@ -44,7 +56,7 @@ const OAuthAddApp = (): ReactElement => { - + {t('Active')} { defaultValue={false} render={({ field }): ReactElement => } /> - + - {t('Application_Name')} - + {t('Application_Name')} + - - {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} - {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} + + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} + {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} - {t('Redirect_URI')} - + {t('Redirect_URI')} + - - {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} - {errors?.redirectUri && {t('error-the-field-is-required', { field: t('Redirect_URI') })}} + + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} + {errors?.redirectUri && {t('error-the-field-is-required', { field: t('Redirect_URI') })}} - + - + diff --git a/apps/meteor/client/views/admin/permissions/RoleForm.tsx b/apps/meteor/client/views/admin/permissions/RoleForm.tsx index 346b1064db1fb..e1517d7721fea 100644 --- a/apps/meteor/client/views/admin/permissions/RoleForm.tsx +++ b/apps/meteor/client/views/admin/permissions/RoleForm.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Box, Field, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, FieldError, FieldHint, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; @@ -31,22 +31,22 @@ const RoleForm = ({ className, editing = false, isProtected = false, isDisabled return ( <> - {t('Role')} - + {t('Role')} + - - {errors?.name && {t('error-the-field-is-required', { field: t('Role') })}} + + {errors?.name && {t('error-the-field-is-required', { field: t('Role') })}} - {t('Description')} - + {t('Description')} + - - Leave the description field blank if you dont want to show the role + + Leave the description field blank if you dont want to show the role - {t('Scope')} - + {t('Scope')} + )} /> - + - {t('Users must use Two Factor Authentication')} - + {t('Users must use Two Factor Authentication')} + } /> - + diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx index c91f530cca3a9..73de11407686d 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx @@ -1,5 +1,5 @@ import type { IRole, IRoom } from '@rocket.chat/core-typings'; -import { Box, Field, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -71,8 +71,8 @@ const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { {role.scope !== 'Users' && ( - {t('Choose_a_room')} - + {t('Choose_a_room')} + { )} /> - + )} - {t('Add_users')} - + {t('Add_users')} + { {t('Add')} - +
diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index 3500a255d95be..8130f2e9ec8b5 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -1,19 +1,29 @@ import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Box, Button, ButtonGroup, TextInput, Field, ToggleSwitch, TextAreaInput } from '@rocket.chat/fuselage'; +import { + Box, + Button, + ButtonGroup, + TextInput, + Field, + FieldLabel, + FieldRow, + FieldHint, + ToggleSwitch, + TextAreaInput, +} from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericModal from '../../../components/GenericModal'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/DeleteTeam'; +import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; type EditRoomProps = { room: Pick; @@ -54,11 +64,6 @@ const getInitialValues = (room: Pick): EditRoomFormV const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); - const [deleting, setDeleting] = useState(false); - - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = @@ -108,9 +113,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => const changeArchivation = archived !== !!room.archived; - const roomsRoute = useRoute('admin-rooms'); - - const canDelete = usePermission(`delete-${room.t}`); + const { handleDelete, canDeleteRoom, isDeleting } = useDeleteRoom(room, { reload: onDelete }); const archiveSelector = room.archived ? 'unarchive' : 'archive'; const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; @@ -150,61 +153,6 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); - const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); - - const handleDelete = useMutableCallback(() => { - const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { - try { - setDeleting(true); - setModal(null); - await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - if (room.teamMain) { - setModal( - setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise => { - try { - setDeleting(true); - setModal(null); - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - setModal( - setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - , - ); - }); - return ( <> e.preventDefault())}> @@ -214,97 +162,97 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement =>
)} - {t('Name')} - - - + {t('Name')} + + + {room.t !== 'd' && ( <> {room.u && ( - {t('Owner')} - + {t('Owner')} + {room.u?.username} - + )} {canViewDescription && ( - {t('Description')} - + {t('Description')} + - + )} {canViewAnnouncement && ( - {t('Announcement')} - + {t('Announcement')} + - + )} {canViewTopic && ( - {t('Topic')} - - - + {t('Topic')} + + + )} {canViewType && ( - {t('Private')} - - - + {t('Private')} + + + - {t('Just_invited_people_can_access_this_channel')} + {t('Just_invited_people_can_access_this_channel')} )} {canViewReadOnly && ( - {t('Read_only')} - - - + {t('Read_only')} + + + - {t('Only_authorized_users_can_write_new_messages')} + {t('Only_authorized_users_can_write_new_messages')} )} {readOnly && ( - {t('React_when_read_only')} - + {t('React_when_read_only')} + - + - {t('React_when_read_only_changed_successfully')} + {t('React_when_read_only_changed_successfully')} )} {canViewArchived && ( - {t('Room_archivation_state_true')} - - - + {t('Room_archivation_state_true')} + + + )} @@ -312,40 +260,40 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => )} - {t('Default')} - - - + {t('Default')} + + + - {t('Favorite')} - - - + {t('Favorite')} + + + - {t('Featured')} - - - + {t('Featured')} + + + - - - diff --git a/apps/meteor/client/views/admin/rooms/RoomRow.tsx b/apps/meteor/client/views/admin/rooms/RoomRow.tsx new file mode 100644 index 0000000000000..2c0dbb8c31a6c --- /dev/null +++ b/apps/meteor/client/views/admin/rooms/RoomRow.tsx @@ -0,0 +1,90 @@ +import { isDiscussion } from '@rocket.chat/core-typings'; +import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { Box, Icon } from '@rocket.chat/fuselage'; +import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; +import RoomAvatar from '../../../components/avatar/RoomAvatar'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; + +const roomTypeI18nMap = { + l: 'Omnichannel', + c: 'Channel', + d: 'Direct_Message', + p: 'Private_Channel', +} as const; + +const getRoomDisplayName = (room: Pick): string | undefined => + room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room); + +const RoomRow = ({ room }: { room: Pick }) => { + const t = useTranslation(); + const mediaQuery = useMediaQuery('(min-width: 1024px)'); + const router = useRouter(); + + const { _id, t: type, usersCount, msgs, default: isDefault, featured, ...args } = room; + const icon = roomCoordinator.getRoomDirectives(room.t).getIcon?.(room); + const roomName = getRoomDisplayName(room); + + const getRoomType = ( + room: Pick, + ): (typeof roomTypeI18nMap)[keyof typeof roomTypeI18nMap] | 'Teams_Public_Team' | 'Teams_Private_Team' | 'Discussion' => { + if (room.teamMain) { + return room.t === 'c' ? 'Teams_Public_Team' : 'Teams_Private_Team'; + } + if (isDiscussion(room)) { + return 'Discussion'; + } + return roomTypeI18nMap[(room as IRoom).t as keyof typeof roomTypeI18nMap]; + }; + + const onClick = useCallback( + (rid) => (): void => + router.navigate({ + name: 'admin-rooms', + params: { + context: 'edit', + id: rid, + }, + }), + [router], + ); + + return ( + + + + + + {icon && } + + {roomName} + + + + + + + {t(getRoomType(room))} + + + {usersCount} + {mediaQuery && {msgs}} + {mediaQuery && {isDefault ? t('True') : t('False')}} + {mediaQuery && {featured ? t('True') : t('False')}} + + ); +}; + +export default RoomRow; diff --git a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx index 6b2c4435c8fa4..094ccb95857ab 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx @@ -1,68 +1,36 @@ -import { type IRoom, isDiscussion, isPublicRoom } from '@rocket.chat/core-typings'; -import { Box, Icon, Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; +import { Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; import { useMediaQuery, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; -import { useEndpoint, useRouter, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { CSSProperties, ReactElement, MutableRefObject } from 'react'; -import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react'; +import type { ReactElement, MutableRefObject } from 'react'; +import React, { useRef, useState, useEffect, useMemo } from 'react'; import GenericNoResults from '../../../components/GenericNoResults'; import { GenericTable, GenericTableBody, - GenericTableCell, GenericTableHeader, GenericTableHeaderCell, GenericTableLoadingTable, - GenericTableRow, } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import RoomAvatar from '../../../components/avatar/RoomAvatar'; -import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import RoomRow from './RoomRow'; import RoomsTableFilters from './RoomsTableFilters'; -import { useFilteredTypeRooms } from './useFilteredTypeRooms'; -import { useFilteredVisibilityRooms } from './useFilteredVisibilityRooms'; - -const style: CSSProperties = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; type RoomFilters = { searchText: string; types: OptionProp[]; - visibility: OptionProp[]; }; const DEFAULT_TYPES = ['d', 'p', 'c', 'l', 'discussions', 'teams']; -const roomTypeI18nMap = { - l: 'Omnichannel', - c: 'Channel', - d: 'Direct_Message', - p: 'Private_Channel', -} as const; - -const getRoomType = ( - room: IRoom, -): (typeof roomTypeI18nMap)[keyof typeof roomTypeI18nMap] | 'Teams_Public_Team' | 'Teams_Private_Team' | 'Discussion' => { - if (room.teamMain) { - return room.t === 'c' ? 'Teams_Public_Team' : 'Teams_Private_Team'; - } - if (isDiscussion(room)) { - return 'Discussion'; - } - return roomTypeI18nMap[(room as IRoom).t as keyof typeof roomTypeI18nMap]; -}; - -const getRoomDisplayName = (room: IRoom): string | undefined => - room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room); - const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement => { - const mediaQuery = useMediaQuery('(min-width: 1024px)'); - const t = useTranslation(); + const mediaQuery = useMediaQuery('(min-width: 1024px)'); - const [roomFilters, setRoomFilters] = useState({ searchText: '', types: [], visibility: [] }); + const [roomFilters, setRoomFilters] = useState({ searchText: '', types: [] }); const prevRoomFilterText = useRef(roomFilters.searchText); @@ -80,29 +48,15 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, count: itemsPerPage, offset: searchText === prevRoomFilterText.current ? current : 0, - types: DEFAULT_TYPES, + types: roomFilters.types.length ? [...roomFilters.types.map((roomType) => roomType.id)] : DEFAULT_TYPES, }; - }, [searchText, sortBy, sortDirection, itemsPerPage, prevRoomFilterText, current, setCurrent]), + }, [searchText, sortBy, sortDirection, itemsPerPage, current, roomFilters.types, setCurrent]), 500, ); const getAdminRooms = useEndpoint('GET', '/v1/rooms.adminRooms'); - const dispatchToastMessage = useToastMessageDispatch(); - - const { data, refetch, isSuccess, isLoading, isError } = useQuery( - ['rooms', query, 'admin'], - async () => { - const adminRooms = await getAdminRooms(query); - - return { ...adminRooms, rooms: adminRooms.rooms as IRoom[] }; - }, - { - onError: (error) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }, - ); + const { data, refetch, isSuccess, isLoading, isError } = useQuery(['rooms', query, 'admin'], async () => getAdminRooms(query)); useEffect(() => { reload.current = refetch; @@ -112,48 +66,29 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React prevRoomFilterText.current = searchText; }, [searchText]); - const router = useRouter(); - - const onClick = useCallback( - (rid) => (): void => - router.navigate({ - name: 'admin-rooms', - params: { - context: 'edit', - id: rid, - }, - }), - [router], - ); - - const headers = useMemo( - () => - [ - - {t('Name')} - , - - {t('Type')} - , - - {t('Visibility')} - , - - {t('Users')} - , - mediaQuery && ( + const headers = ( + <> + + {t('Name')} + + + {t('Type')} + + + {t('Users')} + + {mediaQuery && ( + <> {t('Msgs')} - ), - mediaQuery && ( void> }): React > {t('Default')} - ), - mediaQuery && ( void> }): React > {t('Featured')} - ), - ].filter(Boolean), - [sortDirection, sortBy, setSort, t, mediaQuery], - ); - - const renderRow = useCallback( - (room: IRoom) => { - const { _id, t: type, usersCount, msgs, default: isDefault, featured, ...args } = room; - const visibility = isPublicRoom(room) ? 'Public' : 'Private'; - const icon = roomCoordinator.getRoomDirectives(room.t).getIcon?.(room); - const roomName = getRoomDisplayName(room); - - return ( - - - - - - - {icon && } - - {roomName} - - - - - - - - {t(getRoomType(room))} - - - - - - {t(visibility)} - - - - {usersCount} - {mediaQuery && {msgs}} - {mediaQuery && {isDefault ? t('True') : t('False')}} - {mediaQuery && {featured ? t('True') : t('False')}} - - ); - }, - [mediaQuery, onClick, t], + + )} + ); - function intersectArraysWithoutDuplicates(array1: IRoom[], array2: IRoom[]) { - const set2 = new Set(array2); - - return [...new Set(array1)].filter((item) => set2.has(item)); - } - - const roomsTypeList = useFilteredTypeRooms(roomFilters.types, isLoading, data?.rooms); - const roomsVisibilityList = useFilteredVisibilityRooms(roomFilters.visibility, isLoading, data?.rooms); - - const roomsList = intersectArraysWithoutDuplicates(roomsTypeList, roomsVisibilityList); - return ( <> - {isLoading && ( {headers} - + )} - {isSuccess && data && data?.rooms.length > 0 && ( + {isSuccess && data.rooms.length === 0 && } + {isSuccess && data.rooms.length > 0 && ( <> {headers} - {isSuccess && roomsList?.map((room) => renderRow(room))} + + {data.rooms?.map((room) => ( + + ))} + void> }): React /> )} - {isSuccess && data && data.rooms.length === 0 && } {isError && ( diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index 0d8e5bd0c97ee..fcdaa29c9dffb 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -2,7 +2,7 @@ import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; import type { OptionProp } from '@rocket.chat/ui-client'; import { MultiSelectCustom } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import type { Dispatch, ReactElement, SetStateAction } from 'react'; const roomTypeFilterStructure = [ @@ -12,12 +12,7 @@ const roomTypeFilterStructure = [ isGroupTitle: true, }, { - id: 'channels', - text: 'Channels', - checked: false, - }, - { - id: 'directMessages', + id: 'd', text: 'Direct_Message', checked: false, }, @@ -27,31 +22,23 @@ const roomTypeFilterStructure = [ checked: false, }, { - id: 'omnichannel', + id: 'l', text: 'Omnichannel', checked: false, }, { - id: 'teams', - text: 'Teams', + id: 'p', + text: 'Private_Channels', checked: false, }, -] as OptionProp[]; - -const roomVisibilityFilterStructure = [ - { - id: 'filter_by_visibility', - text: 'Filter_by_visibility', - isGroupTitle: true, - }, { - id: 'private', - text: 'Private', + id: 'c', + text: 'Public_Channels', checked: false, }, { - id: 'public', - text: 'Public', + id: 'teams', + text: 'Teams', checked: false, }, ] as OptionProp[]; @@ -59,16 +46,25 @@ const roomVisibilityFilterStructure = [ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch> }): ReactElement => { const t = useTranslation(); const [text, setText] = useState(''); - const [roomTypeOptions, setRoomTypeOptions] = useState(roomTypeFilterStructure); - const [roomVisibilityOptions, setRoomVisibilityOptions] = useState(roomVisibilityFilterStructure); + const [roomTypeSelectedOptions, setRoomTypeSelectedOptions] = useState([]); - const [roomVisibilitySelectedOptions, setRoomVisibilitySelectedOptions] = useState([]); - useEffect(() => { - return setFilters({ searchText: text, types: roomTypeSelectedOptions, visibility: roomVisibilitySelectedOptions }); - }, [setFilters, roomTypeSelectedOptions, roomVisibilitySelectedOptions, text]); + const handleSearchTextChange = useCallback( + (event) => { + const text = event.currentTarget.value; + setFilters({ searchText: text, types: roomTypeSelectedOptions }); + setText(text); + }, + [roomTypeSelectedOptions, setFilters], + ); - const handleSearchTextChange = useCallback((event) => setText(event.currentTarget.value), []); + const handleRoomTypeChange = useCallback( + (options: OptionProp[]) => { + setFilters({ searchText: text, types: options }); + setRoomTypeSelectedOptions(options); + }, + [text, setFilters], + ) as Dispatch>; return ( - - - - diff --git a/apps/meteor/client/views/admin/rooms/useFilteredTypeRooms.tsx b/apps/meteor/client/views/admin/rooms/useFilteredTypeRooms.tsx deleted file mode 100644 index 7114aa4f35c9a..0000000000000 --- a/apps/meteor/client/views/admin/rooms/useFilteredTypeRooms.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isDiscussion, isTeamRoom, isDirectMessageRoom } from '@rocket.chat/core-typings'; -import type { OptionProp } from '@rocket.chat/ui-client'; - -const filterRoomsByChannels = (room: Partial): boolean => - (room.t === 'c' || room.t === 'p') && !isDiscussion(room) && !isTeamRoom(room); // can be a public channel or a private channel (group) -const filterRoomsByDirectMessages = (room: Partial): boolean => isDirectMessageRoom(room); -const filterRoomsByDiscussions = (room: Partial): boolean => isDiscussion(room); -const filterRoomsByOmnichannel = ({ t }: Partial): boolean => t === 'l'; // LiveChat -const filterRoomsByTeams = (room: Partial): boolean => isTeamRoom(room); - -const filters: Record) => boolean> = { - channels: filterRoomsByChannels, - directMessages: filterRoomsByDirectMessages, - discussions: filterRoomsByDiscussions, - omnichannel: filterRoomsByOmnichannel, - teams: filterRoomsByTeams, -}; - -export const useFilteredTypeRooms = (selectedOptions: OptionProp[], isLoading: boolean, rooms?: IRoom[]) => { - if (isLoading || !rooms) return []; - if (selectedOptions.length === 0) return rooms; - - let filtered: IRoom[] = []; - - selectedOptions.forEach((option) => { - filtered = [...new Set([...filtered, ...rooms.filter(filters[option.id])])]; - }); - - return filtered; -}; diff --git a/apps/meteor/client/views/admin/rooms/useFilteredVisibilityRooms.tsx b/apps/meteor/client/views/admin/rooms/useFilteredVisibilityRooms.tsx deleted file mode 100644 index be40648170018..0000000000000 --- a/apps/meteor/client/views/admin/rooms/useFilteredVisibilityRooms.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isPublicRoom } from '@rocket.chat/core-typings'; -import type { OptionProp } from '@rocket.chat/ui-client'; - -const filterRoomsByPrivate = (room: Partial): boolean => !isPublicRoom(room); -const filterRoomsByPublic = (room: Partial): boolean => isPublicRoom(room); - -const filters: Record) => boolean> = { - private: filterRoomsByPrivate, - public: filterRoomsByPublic, -}; - -export const useFilteredVisibilityRooms = (selectedOptions: OptionProp[], isLoading: boolean, rooms?: IRoom[]) => { - if (isLoading || !rooms) return []; - if (selectedOptions.length === 0) return rooms; - - let filtered: IRoom[] = []; - - selectedOptions.forEach((option) => { - filtered = [...new Set([...filtered, ...rooms.filter(filters[option.id])])]; - }); - - return filtered; -}; diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index fa418b986cc1e..bea10777d66b6 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -70,8 +70,8 @@ declare module '@rocket.chat/ui-contexts' { pattern: '/admin/registration/:page?'; }; 'admin-view-logs': { - pathname: '/admin/logs'; - pattern: '/admin/logs'; + pathname: '/admin/reports'; + pattern: '/admin/reports'; }; 'federation-dashboard': { pathname: '/admin/federation'; @@ -193,7 +193,7 @@ registerAdminRoute('/registration/:page?', { component: lazy(() => import('./cloud/CloudRoute')), }); -registerAdminRoute('/logs', { +registerAdminRoute('/reports', { name: 'admin-view-logs', component: lazy(() => import('./viewLogs/ViewLogsRoute')), }); diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx index 72b97bf446d4c..a7433afc66c25 100644 --- a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx +++ b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx @@ -1,5 +1,5 @@ import type { ISettingBase, SettingEditor, SettingValue } from '@rocket.chat/core-typings'; -import { Box, Callout, Field, Margins } from '@rocket.chat/fuselage'; +import { Box, Callout, Field, FieldHint, Margins } from '@rocket.chat/fuselage'; import type { ElementType, ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; @@ -95,7 +95,7 @@ const MemoizedSetting = ({ {...inputProps} disabled={disabled} /> - {hint && type !== 'code' && {hint}} + {hint && type !== 'code' && {hint}} {callout && ( {callout} diff --git a/apps/meteor/client/views/admin/settings/SettingSkeleton.tsx b/apps/meteor/client/views/admin/settings/SettingSkeleton.tsx index 62e34c069edf2..a831fca6db966 100644 --- a/apps/meteor/client/views/admin/settings/SettingSkeleton.tsx +++ b/apps/meteor/client/views/admin/settings/SettingSkeleton.tsx @@ -1,17 +1,17 @@ -import { Field, Flex, InputBox, Skeleton } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, Flex, InputBox, Skeleton } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; const SettingSkeleton = (): ReactElement => ( - + - + - + - + ); diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx index 8bf75c3753d54..26331379a9fd9 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx @@ -1,7 +1,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Button, Box } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardTitle, CardFooter } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -28,13 +28,13 @@ const SettingsGroupCard = ({ id, title, description }: SettingsGroupCardProps): return ( - {t(title)} - + {t(title)} + {description && t.has(description) && } - - + + - + ); }; diff --git a/apps/meteor/client/views/admin/settings/groups/CreateOAuthModal.tsx b/apps/meteor/client/views/admin/settings/groups/CreateOAuthModal.tsx index d09b953056c65..53ecc93609b5b 100644 --- a/apps/meteor/client/views/admin/settings/groups/CreateOAuthModal.tsx +++ b/apps/meteor/client/views/admin/settings/groups/CreateOAuthModal.tsx @@ -1,4 +1,4 @@ -import { TextInput, Field, Box } from '@rocket.chat/fuselage'; +import { TextInput, Field, FieldLabel, FieldRow, FieldError, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, FormEvent, SyntheticEvent } from 'react'; import React, { useState } from 'react'; @@ -34,8 +34,8 @@ const CreateOAuthModal = ({ onConfirm, onClose }: CreateOAuthModalProps): ReactE onConfirm={handleConfirm} > - {t('Give_a_unique_name_for_the_custom_oauth')} - + {t('Give_a_unique_name_for_the_custom_oauth')} + - - {error && {error}} + + {error && {error}} ); diff --git a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx index 900351877027a..bea9ccd762220 100644 --- a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -1,5 +1,5 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { Button, Box, TextInput, Field } from '@rocket.chat/fuselage'; +import { Button, Box, TextInput, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useSetting, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; @@ -110,12 +110,12 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { > - {t('LDAP_Username_To_Search')} + {t('LDAP_Username_To_Search')} - + - + , ); diff --git a/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx b/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx index 7164469aae964..09ecfd5c25193 100644 --- a/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx +++ b/apps/meteor/client/views/admin/settings/groups/voip/AssignAgentModal.tsx @@ -1,4 +1,4 @@ -import { Button, Modal, Select, Field, FieldGroup, Box } from '@rocket.chat/fuselage'; +import { Button, Modal, Select, Field, FieldGroup, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; @@ -46,14 +46,14 @@ const AssignAgentModal: FC = ({ existingExtension, close - {t('Agent_Without_Extensions')} - + {t('Agent_Without_Extensions')} + - + - {t('Free_Extension_Numbers')} - + {t('Free_Extension_Numbers')} + handleChange(String(value))} options={languages.map(({ key, name }) => [key, name])} /> - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx index 8b7175320d984..15a1aab443d74 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.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 { PathPattern } from '@rocket.chat/rest-typings'; import type { ReactElement } from 'react'; import React from 'react'; @@ -45,13 +45,13 @@ function LookupSettingInput({ <> - + {label} - + {hasResetButton && } - + handleChange(String(value))} options={values.map(({ key, i18nLabel }) => [key, t(i18nLabel)])} /> - + ); } diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx index 1478b87465831..12c5da28365c2 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.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 moment from 'moment-timezone'; import type { ReactElement } from 'react'; import React from 'react'; @@ -38,13 +38,13 @@ function SelectTimezoneSettingInput({ <> - + {label} - + {hasResetButton && } - + = ({ data, userDepartments, availableDepartm placeholder={t('Select_an_option')} onChange={handleStatus} /> - + {MaxChats && } {voipEnabled && ( - {t('VoIP_Extension')} - + {t('VoIP_Extension')} + - + )} diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx index e030fe3d63053..47d60b3d99581 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.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'; @@ -38,13 +38,13 @@ const AddAgent = ({ reload }: AddAgentProps): ReactElement => { return ( - {t('Username')} - + {t('Username')} + - + ); diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx index a636433eaacb6..215155e97cca1 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx @@ -100,7 +100,7 @@ const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { )} {isSuccess && data?.users.length > 0 && ( <> - + {headers} {data?.users.map((user) => ( diff --git a/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx b/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx index 333d2e772c4cb..4a7405b7b8ccf 100644 --- a/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Box, Select, Margins, Field, Label } from '@rocket.chat/fuselage'; +import { Box, Select, Margins, Field, FieldLabel, FieldRow, Label } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useState, useEffect } from 'react'; @@ -74,10 +74,10 @@ const AnalyticsPage = () => { - {t('Chart')} - + {t('Chart')} + - + {conditionValuePlaceholder && ( - + - + )} - {t('Action')} - + {t('Action')} + - - + + handleChangeLanguage(String(value))} options={languages} /> - + diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx index 6bfc4ff03a80b..d31e8bde76b9a 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx @@ -2,9 +2,11 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useLanguage } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo, useEffect, useState, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { dispatchToastMessage } from '../../../../lib/toast'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import AutoTranslate from './AutoTranslate'; @@ -16,10 +18,12 @@ const AutoTranslateWithData = (): ReactElement => { const userLanguage = useLanguage(); const [currentLanguage, setCurrentLanguage] = useState(subscription?.autoTranslateLanguage ?? ''); const saveSettings = useEndpointAction('POST', '/v1/autotranslate.saveSettings'); + const { t } = useTranslation(); const { value: translateData } = useEndpointData('/v1/autotranslate.getSupportedLanguages', { params: useMemo(() => ({ targetLanguage: userLanguage }), [userLanguage]), }); + const languagesDict = translateData ? Object.fromEntries(translateData.languages.map((lang) => [lang.language, lang.name])) : {}; const handleChangeLanguage = useMutableCallback((value) => { setCurrentLanguage(value); @@ -29,6 +33,10 @@ const AutoTranslateWithData = (): ReactElement => { field: 'autoTranslateLanguage', value, }); + dispatchToastMessage({ + type: 'success', + message: t('AutoTranslate_language_set_to', { language: languagesDict[value] }), + }); }); const handleSwitch = useMutableCallback((event) => { @@ -37,6 +45,18 @@ const AutoTranslateWithData = (): ReactElement => { field: 'autoTranslate', value: event.target.checked, }); + dispatchToastMessage({ + type: 'success', + message: event.target.checked + ? t('AutoTranslate_Enabled_for_room', { roomName: room.name }) + : t('AutoTranslate_Disabled_for_room', { roomName: room.name }), + }); + if (event.target.checked && currentLanguage) { + dispatchToastMessage({ + type: 'success', + message: t('AutoTranslate_language_set_to', { language: languagesDict[currentLanguage] }), + }); + } }); useEffect(() => { diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index 1bf59d4e8de78..28b8b56a1df22 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, Select, FieldGroup } from '@rocket.chat/fuselage'; +import { Field, Select, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState, useMemo } from 'react'; @@ -40,10 +40,10 @@ const ExportMessages = () => { - {t('Method')} - + {t('Method')} + - + diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx index f44e94ed69cc3..c8ae5a96300d7 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; -import { Field, TextInput, ButtonGroup, Button, Box, Icon, Callout, FieldGroup } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, TextInput, ButtonGroup, Button, Box, Icon, Callout, FieldGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useUserRoom, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, MouseEventHandler } from 'react'; @@ -116,27 +116,27 @@ const MailExportForm: FC = ({ onCancel, rid }) => { - {t('To_users')} - + {t('To_users')} + - + - {t('To_additional_emails')} - + {t('To_additional_emails')} + } /> - + - {t('Subject')} - + {t('Subject')} + } /> - + {errorMessage && {errorMessage}} diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js deleted file mode 100644 index be7a8b1e42384..0000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ /dev/null @@ -1,511 +0,0 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { - Field, - TextInput, - PasswordInput, - ToggleSwitch, - MultiSelect, - Accordion, - Callout, - NumberInput, - FieldGroup, - Button, - ButtonGroup, - Box, - TextAreaInput, -} from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { - useSetModal, - useSetting, - usePermission, - useAtLeastOnePermission, - useRole, - useMethod, - useTranslation, - useRouter, -} from '@rocket.chat/ui-contexts'; -import React, { useCallback, useMemo, useRef } from 'react'; - -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; -import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; -import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; -import { - ContextualbarHeader, - ContextualbarBack, - ContextualbarTitle, - ContextualbarClose, - ContextualbarScrollableContent, - ContextualbarFooter, -} from '../../../../../components/Contextualbar'; -import GenericModal from '../../../../../components/GenericModal'; -import RawText from '../../../../../components/RawText'; -import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; -import { useEndpointAction } from '../../../../../hooks/useEndpointAction'; -import { useForm } from '../../../../../hooks/useForm'; -import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; - -const typeMap = { - c: 'Channels', - p: 'Groups', - d: 'DMs', -}; - -const useInitialValues = (room, settings) => { - const { - t, - ro, - archived, - topic, - description, - announcement, - joinCodeRequired, - sysMes, - encrypted, - retention = {}, - reactWhenReadOnly, - } = room; - - const { retentionPolicyEnabled, maxAgeDefault } = settings; - - const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${typeMap[room.t]}`); - const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); - const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); - - return useMemo( - () => ({ - roomName: t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, { type: t, ...room }), - roomType: t, - readOnly: !!ro, - reactWhenReadOnly, - archived: !!archived, - roomTopic: topic ?? '', - roomDescription: description ?? '', - roomAnnouncement: announcement ?? '', - roomAvatar: undefined, - joinCode: '', - joinCodeRequired: !!joinCodeRequired, - systemMessages: Array.isArray(sysMes) ? sysMes : [], - hideSysMes: !!sysMes?.length, - encrypted, - ...(retentionPolicyEnabled && { - retentionEnabled: retention.enabled ?? retentionEnabledDefault, - retentionOverrideGlobal: !!retention.overrideGlobal, - retentionMaxAge: Math.min(retention.maxAge, maxAgeDefault) || maxAgeDefault, - retentionExcludePinned: retention.excludePinned ?? excludePinnedDefault, - retentionFilesOnly: retention.filesOnly ?? filesOnlyDefault, - }), - }), - [ - announcement, - archived, - description, - excludePinnedDefault, - filesOnlyDefault, - joinCodeRequired, - maxAgeDefault, - retention.enabled, - retention.excludePinned, - retention.filesOnly, - retention.maxAge, - retention.overrideGlobal, - retentionEnabledDefault, - retentionPolicyEnabled, - ro, - room, - sysMes, - t, - topic, - encrypted, - reactWhenReadOnly, - ], - ); -}; - -const getCanChangeType = (room, canCreateChannel, canCreateGroup, isAdmin) => - (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); - -function EditChannel({ room, onClickClose, onClickBack }) { - const t = useTranslation(); - - const setModal = useSetModal(); - - const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); - const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${typeMap[room.t]}`) || 30; - - const saveData = useRef({}); - const router = useRouter(); - - const onChange = useCallback(({ initialValue, value, key }) => { - const { current } = saveData; - if (JSON.stringify(initialValue) !== JSON.stringify(value)) { - if (key === 'systemMessages' && value?.length > 0) { - current.hideSysMes = true; - } - current[key] = value; - } else { - delete current[key]; - } - }, []); - - const { values, handlers, hasUnsavedChanges, reset, commit } = useForm( - useInitialValues(room, { retentionPolicyEnabled, maxAgeDefault }), - onChange, - ); - - const sysMesOptions = useMemo(() => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel)]), [t]); - - const { - roomName, - roomType, - readOnly, - encrypted, - roomAvatar, - archived, - roomTopic, - roomDescription, - roomAnnouncement, - reactWhenReadOnly, - joinCode, - joinCodeRequired, - systemMessages, - hideSysMes, - retentionEnabled, - retentionOverrideGlobal, - retentionMaxAge, - retentionExcludePinned, - retentionFilesOnly, - } = values; - - const { - handleJoinCode, - handleJoinCodeRequired, - handleSystemMessages, - handleEncrypted, - handleHideSysMes, - handleRoomName, - handleReadOnly, - handleArchived, - handleRoomAvatar, - handleReactWhenReadOnly, - handleRoomType, - handleRoomTopic, - handleRoomDescription, - handleRoomAnnouncement, - handleRetentionEnabled, - handleRetentionOverrideGlobal, - handleRetentionMaxAge, - handleRetentionExcludePinned, - handleRetentionFilesOnly, - } = handlers; - - const [ - canViewName, - canViewTopic, - canViewAnnouncement, - canViewArchived, - canViewDescription, - canViewType, - canViewReadOnly, - canViewHideSysMes, - canViewJoinCode, - canViewEncrypted, - ] = useMemo(() => { - const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange || (() => {}); - return [ - isAllowed(room, RoomSettingsEnum.NAME), - isAllowed(room, RoomSettingsEnum.TOPIC), - isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), - isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), - isAllowed(room, RoomSettingsEnum.DESCRIPTION), - isAllowed(room, RoomSettingsEnum.TYPE), - isAllowed(room, RoomSettingsEnum.READ_ONLY), - isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), - isAllowed(room, RoomSettingsEnum.JOIN_CODE), - isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), - isAllowed(room, RoomSettingsEnum.E2E), - ]; - }, [room]); - - const isAdmin = useRole('admin'); - - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); - const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); - const canSetRo = usePermission('set-readonly', room._id); - const canSetReactWhenRo = usePermission('set-react-when-readonly', room._id); - const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const canArchiveOrUnarchive = useAtLeastOnePermission( - useMemo(() => ['archive-room', 'unarchive-room'], []), - room._id, - ); - const canDelete = usePermission(`delete-${room.t}`); - const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); - - const changeArchivation = archived !== !!room.archived; - const archiveSelector = room.archived ? 'unarchive' : 'archive'; - const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; - const saveAction = useEndpointAction('POST', '/v1/rooms.saveRoomSettings', { - successMessage: t('Room_updated_successfully'), - }); - const archiveAction = useEndpointAction('POST', '/v1/rooms.changeArchivationState', { successMessage: t(archiveMessage) }); - - const handleSave = useMutableCallback(async () => { - const { joinCodeRequired, hideSysMes, ...data } = saveData.current; - delete data.archived; - const save = () => - saveAction({ - rid: room._id, - ...data, - ...(joinCode && { joinCode: joinCodeRequired ? joinCode : '' }), - ...((data.systemMessages || !hideSysMes) && { - systemMessages: hideSysMes && systemMessages, - }), - }); - - const archive = () => archiveAction({ rid: room._id, action: archiveSelector }); - - await Promise.all([hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean)); - saveData.current = {}; - commit(); - }); - - const deleteRoom = useMethod('eraseRoom'); - - const handleDelete = useMutableCallback(() => { - const onCancel = () => setModal(undefined); - const onConfirm = async () => { - await deleteRoom(room._id); - onCancel(); - router.navigate('/home'); - }; - - setModal( - - {t('Delete_Room_Warning')} - , - ); - }); - - const changeRoomType = useMutableCallback(() => { - handleRoomType(roomType === 'p' ? 'c' : 'p'); - }); - - const onChangeMaxAge = useMutableCallback((e) => { - handleRetentionMaxAge(Math.max(1, Number(e.currentTarget.value))); - }); - - const isFederated = useMemo(() => isRoomFederated(room), [room]); - - return ( - <> - - {onClickBack && } - {room.teamId ? t('edit-team') : t('edit-room')} - {onClickClose && } - - e.preventDefault())}> - - - - - {t('Name')} - - - - - {canViewDescription && ( - - {t('Description')} - - - - - )} - {canViewAnnouncement && ( - - {t('Announcement')} - - - - - )} - {canViewTopic && ( - - {t('Topic')} - - - - - )} - {canViewType && ( - - - {t('Private')} - - - - - {t('Teams_New_Private_Description_Enabled')} - - )} - {canViewReadOnly && ( - - - {t('Read_only')} - - - - - {t('Only_authorized_users_can_write_new_messages')} - - )} - {readOnly && ( - - - {t('React_when_read_only')} - - - - - {t('Only_authorized_users_can_react_to_messages')} - - )} - {canViewArchived && ( - - - {t('Room_archivation_state_true')} - - - - - - )} - {canViewJoinCode && ( - - - {t('Password_to_access')} - - - - - - - - - )} - {canViewHideSysMes && ( - - - {t('Hide_System_Messages')} - - - - - - - - - )} - {canViewEncrypted && ( - - - {t('Encrypted')} - - - - - - )} - {retentionPolicyEnabled && ( - - - - - - {t('RetentionPolicyRoom_Enabled')} - - - - - - - - {t('RetentionPolicyRoom_OverrideGlobal')} - - - - - - {retentionOverrideGlobal && ( - <> - - {t('RetentionPolicyRoom_ReadTheDocs')} - - - {t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })} - - - - - - - {t('RetentionPolicyRoom_ExcludePinned')} - - - - - - - - {t('RetentionPolicyRoom_FilesOnly')} - - - - - - - )} - - - - )} - - - - - - - - - - - - ); -} - -export default EditChannel; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js deleted file mode 100644 index e64dcc17562a3..0000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useRoom } from '../../../contexts/RoomContext'; -import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; -import EditChannel from './EditChannel'; - -function EditChannelWithData({ onClickBack }) { - const room = useRoom(); - const { closeTab } = useRoomToolbox(); - - return ; -} - -export default EditChannelWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx new file mode 100644 index 0000000000000..b2c552fd3d87a --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -0,0 +1,487 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Field, + FieldRow, + FieldLabel, + FieldHint, + TextInput, + PasswordInput, + ToggleSwitch, + MultiSelect, + Accordion, + Callout, + NumberInput, + FieldGroup, + Button, + ButtonGroup, + Box, + TextAreaInput, +} from '@rocket.chat/fuselage'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../components/Contextualbar'; +import RawText from '../../../../../components/RawText'; +import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; +import { getDirtyFields } from '../../../../../lib/getDirtyFields'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; +import { useEditRoomInitialValues } from './useEditRoomInitialValues'; +import { useEditRoomPermissions } from './useEditRoomPermissions'; + +type EditRoomInfoProps = { + room: IRoomWithRetentionPolicy; + onClickClose: () => void; + onClickBack: () => void; +}; + +const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isFederated = useMemo(() => isRoomFederated(room), [room]); + + const retentionPolicy = useSetting('RetentionPolicy_Enabled'); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); + const defaultValues = useEditRoomInitialValues(room); + + const { + watch, + reset, + control, + handleSubmit, + formState: { isDirty, dirtyFields, errors }, + } = useForm({ mode: 'onBlur', defaultValues }); + + const sysMesOptions: SelectOption[] = useMemo( + () => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel as TranslationKey)]), + [t], + ); + + const { readOnly, archived, joinCodeRequired, hideSysMes, retentionEnabled, retentionMaxAge, retentionOverrideGlobal } = watch(); + + const { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + } = useEditRoomPermissions(room); + + const changeArchiving = archived !== !!room.archived; + + const saveAction = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); + const archiveAction = useEndpoint('POST', '/v1/rooms.changeArchivationState'); + + const handleUpdateRoomData = useMutableCallback(async ({ hideSysMes, ...formData }) => { + const data = getDirtyFields(formData, dirtyFields); + + try { + await saveAction({ + rid: room._id, + ...data, + ...(data.joinCode && { joinCode: joinCodeRequired ? data.joinCode : '' }), + ...((data.systemMessages || !hideSysMes) && { + systemMessages: hideSysMes && data.systemMessages, + }), + }); + + dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); + onClickClose(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleArchive = useMutableCallback(async () => { + try { + await archiveAction({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }); + dispatchToastMessage({ type: 'success', message: room.archived ? t('Room_has_been_unarchived') : t('Room_has_been_archived') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleSave = useMutableCallback(async (data) => { + await Promise.all([isDirty && handleUpdateRoomData(data), changeArchiving && handleArchive()].filter(Boolean)); + }); + + const formId = useUniqueId(); + const roomNameField = useUniqueId(); + const roomDescriptionField = useUniqueId(); + const roomAnnouncementField = useUniqueId(); + const roomTopicField = useUniqueId(); + const roomTypeField = useUniqueId(); + const readOnlyField = useUniqueId(); + const reactWhenReadOnlyField = useUniqueId(); + const archivedField = useUniqueId(); + const joinCodeRequiredField = useUniqueId(); + const hideSysMesField = useUniqueId(); + const encryptedField = useUniqueId(); + const retentionEnabledField = useUniqueId(); + const retentionOverrideGlobalField = useUniqueId(); + const retentionMaxAgeField = useUniqueId(); + const retentionExcludePinnedField = useUniqueId(); + const retentionFilesOnlyField = useUniqueId(); + + return ( + <> + + {onClickBack && } + {room.teamId ? t('edit-team') : t('edit-room')} + {onClickClose && } + + +
+ + } + /> + + + + + {t('Name')} + + + } + /> + + {errors.roomName && {errors.roomName.message}} + + {canViewDescription && ( + + {t('Description')} + + } + /> + + + )} + {canViewAnnouncement && ( + + {t('Announcement')} + + } + /> + + + )} + {canViewTopic && ( + + {t('Topic')} + + } + /> + + + )} + {canViewType && ( + + + {t('Private')} + + ( + onChange(value === 'p' ? 'c' : 'p')} + aria-describedby={`${roomTypeField}-hint`} + /> + )} + /> + + + {t('Teams_New_Private_Description_Enabled')} + + )} + {canViewReadOnly && ( + + + {t('Read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_write_new_messages')} + + )} + {readOnly && ( + + + {t('React_when_read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_react_to_messages')} + + )} + {canViewArchived && ( + + + {t('Room_archivation_state_true')} + + ( + + )} + /> + + + + )} + {canViewJoinCode && ( + + + {t('Password_to_access')} + + ( + + )} + /> + + + + } + /> + + + )} + {canViewHideSysMes && ( + + + {t('Hide_System_Messages')} + + ( + + )} + /> + + + + ( + + )} + /> + + + )} + {canViewEncrypted && ( + + + {t('Encrypted')} + + ( + + )} + /> + + + + )} + + {retentionPolicy && ( + + + + + + {t('RetentionPolicyRoom_Enabled')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_OverrideGlobal')} + + ( + + )} + /> + + + + {retentionOverrideGlobal && ( + <> + + {t('RetentionPolicyRoom_ReadTheDocs')} + + + {t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })} + + ( + onChange(Math.max(1, Number(currentValue)))} + /> + )} + /> + + + + + {t('RetentionPolicyRoom_ExcludePinned')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_FilesOnly')} + + ( + + )} + /> + + + + + )} + + + + )} +
+
+ + + + + + + + + + + ); +}; + +export default EditRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx new file mode 100644 index 0000000000000..ad758c1bc8a6b --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx @@ -0,0 +1,15 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; +import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; +import EditRoomInfo from './EditRoomInfo'; + +const EditRoomInfoWithData = ({ onClickBack }: { onClickBack: () => void }) => { + const room = useRoom() as IRoomWithRetentionPolicy; + const { closeTab } = useRoomToolbox(); + + return ; +}; + +export default EditRoomInfoWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts index 4083ad9a958f7..d8b31e17800cd 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts @@ -1 +1 @@ -export { default } from './EditChannelWithData'; +export { default } from './EditRoomInfoWithData'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts new file mode 100644 index 0000000000000..f36802bb9f562 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -0,0 +1,71 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getPolicyRoomType = (roomType: IRoomWithRetentionPolicy['t']) => { + switch (roomType) { + case 'c': + return 'Channels'; + case 'p': + return 'Groups'; + case 'd': + return 'DMs'; + } +}; + +export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { + const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + + const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); + const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${getPolicyRoomType(room.t)}`) || 30; + const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${getPolicyRoomType(room.t)}`); + const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); + const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); + + return useMemo( + () => ({ + roomName: t === 'd' && room.usernames ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, room), + roomType: t, + readOnly: !!ro, + reactWhenReadOnly, + archived: !!archived, + roomTopic: topic ?? '', + roomDescription: description ?? '', + roomAnnouncement: announcement ?? '', + roomAvatar: undefined, + joinCode: '', + joinCodeRequired: !!joinCodeRequired, + systemMessages: Array.isArray(sysMes) ? sysMes : [], + hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, + encrypted, + ...(retentionPolicyEnabled && { + retentionEnabled: retention?.enabled ?? retentionEnabledDefault, + retentionOverrideGlobal: !!retention?.overrideGlobal, + retentionMaxAge: Math.min(retention?.maxAge, maxAgeDefault) || maxAgeDefault, + retentionExcludePinned: retention?.excludePinned ?? excludePinnedDefault, + retentionFilesOnly: retention?.filesOnly ?? filesOnlyDefault, + }), + }), + [ + announcement, + archived, + description, + excludePinnedDefault, + filesOnlyDefault, + joinCodeRequired, + maxAgeDefault, + retention, + retentionEnabledDefault, + retentionPolicyEnabled, + ro, + room, + sysMes, + t, + topic, + encrypted, + reactWhenReadOnly, + ], + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts new file mode 100644 index 0000000000000..7b9e8c353941a --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -0,0 +1,77 @@ +import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; +import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChannel: boolean, canCreateGroup: boolean, isAdmin: boolean) => + (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); + +export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) => { + const isAdmin = useRole('admin'); + const canCreateChannel = usePermission('create-c'); + const canCreateGroup = usePermission('create-p'); + + const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); + const canSetReadOnly = usePermission('set-readonly', room._id); + const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id); + const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); + const canArchiveOrUnarchive = useAtLeastOnePermission( + useMemo(() => ['archive-room', 'unarchive-room'], []), + room._id, + ); + const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); + + const [ + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + ] = useMemo(() => { + const isAllowed = + roomCoordinator.getRoomDirectives(room.t)?.allowRoomSettingChange || + (() => { + undefined; + }); + return [ + isAllowed(room, RoomSettingsEnum.NAME), + isAllowed(room, RoomSettingsEnum.TOPIC), + isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), + isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), + isAllowed(room, RoomSettingsEnum.DESCRIPTION), + isAllowed(room, RoomSettingsEnum.TYPE), + isAllowed(room, RoomSettingsEnum.READ_ONLY), + isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), + isAllowed(room, RoomSettingsEnum.JOIN_CODE), + isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), + isAllowed(room, RoomSettingsEnum.E2E), + ]; + }, [room]); + + return { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx deleted file mode 100644 index b812e896bab9f..0000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint, usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericModal from '../../../../../../components/GenericModal'; - -// TODO: resetState for TeamsChannels -export const useRoomDelete = (room: IRoom, resetState?: () => void) => { - const t = useTranslation(); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const router = useRouter(); - - const hasPermissionToDelete = usePermission(room.t === 'c' ? 'delete-c' : 'delete-p', room._id); - const canDelete = isRoomFederated(room) ? false : hasPermissionToDelete; - - const deleteRoom = useEndpoint('POST', room.t === 'c' ? '/v1/channels.delete' : '/v1/groups.delete'); - - const handleDelete = useMutableCallback(() => { - const onConfirm = async () => { - try { - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - if (resetState) { - return resetState(); - } - - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(null); - }; - - setModal( - setModal(null)} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} - , - ); - }); - - return canDelete ? handleDelete : null; -}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts index d0ab03bd9ee78..638cd23b66eda 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts @@ -1,10 +1,9 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; import type { IRoom } from '@rocket.chat/core-typings'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; import { useRoomConvertToTeam } from './actions/useRoomConvertToTeam'; -import { useRoomDelete } from './actions/useRoomDelete'; import { useRoomHide } from './actions/useRoomHide'; import { useRoomLeave } from './actions/useRoomLeave'; import { useRoomMoveToTeam } from './actions/useRoomMoveToTeam'; @@ -16,11 +15,10 @@ type RoomActions = { export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: RoomActions, resetState?: () => void) => { const t = useTranslation(); - const isFederated = isRoomFederated(room); const handleHide = useRoomHide(room); const handleLeave = useRoomLeave(room); - const handleDelete = useRoomDelete(room, resetState); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload: resetState }); const handleMoveToTeam = useRoomMoveToTeam(room); const handleConvertToTeam = useRoomConvertToTeam(room); @@ -40,7 +38,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R action: onClickEdit, }, }), - ...(!isFederated && + ...(canDeleteRoom && handleDelete && { delete: { label: t('Delete'), @@ -77,7 +75,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R }, }), }), - [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, isFederated], + [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, canDeleteRoom], ); return memoizedActions; diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx index bc25f538643df..f1f69f9299f3c 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx @@ -1,5 +1,5 @@ import type { IMessageSearchProvider } from '@rocket.chat/core-typings'; -import { Box, Field, Icon, TextInput, ToggleSwitch } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, FieldHint, Icon, TextInput, ToggleSwitch } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -55,7 +55,7 @@ const MessageSearchForm = ({ provider, onSearch }: MessageSearchFormProps) => { > - + } placeholder={t('Search_Messages')} @@ -63,15 +63,15 @@ const MessageSearchForm = ({ provider, onSearch }: MessageSearchFormProps) => { autoComplete='off' {...register('searchText')} /> - - {provider.description && } + + {provider.description && } {globalSearchEnabled && ( - + - {t('Global_Search')} - + {t('Global_Search')} + )} diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationPreference.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationPreference.tsx index f187312e12eaf..c89584bd1eeff 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationPreference.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationPreference.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, Select } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -21,11 +21,11 @@ const NotificationPreference = ({ ...props }: NotificationPreferenceProps): ReactElement => ( - {name} - + {name} + onChange(String(value))} /> - + ); }; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx b/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx index 9560d61c28ae4..5b9fb7433f7fc 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx +++ b/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx @@ -1,5 +1,5 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, Field, TextInput } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, FieldError, FieldDescription, TextInput } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React from 'react'; @@ -36,7 +36,7 @@ const CannedResponseForm: FC<{ return ( <> - {t('Shortcut')} + {t('Shortcut')} - {errors.shortcut} + {errors.shortcut} - + {t('Message')} {previewState ? t('Editor') : t('Preview')} - + {previewState ? : } @@ -63,21 +63,21 @@ const CannedResponseForm: FC<{ {(isManager || isMonitor) && ( <> - {t('Sharing')} - {radioDescription} - + {t('Sharing')} + {radioDescription} + - + {scope === 'department' && ( - {t('Department')} + {t('Department')} - {errors.departmentId} + {errors.departmentId} )} diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx b/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx index 3976e81e671cf..b14e293b5d79e 100644 --- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx +++ b/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx @@ -3,6 +3,8 @@ import { Pagination, Button, Field, + FieldLabel, + FieldRow, Box, States, StatesIcon, @@ -125,13 +127,13 @@ const MonitorsTable = () => { <> - {t('Username')} - + {t('Username')} + void} /> - + {((isSuccess && data?.monitors.length > 0) || queryHasChanged) && setText(text)} />} diff --git a/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx b/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx index 81d0fd72a2e63..042a6a7d24982 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx +++ b/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx @@ -1,5 +1,5 @@ import type { ILivechatPriority, Serialized } from '@rocket.chat/core-typings'; -import { Field, Button, Box, ButtonGroup, Throbber } from '@rocket.chat/fuselage'; +import { Field, FieldError, Button, Box, ButtonGroup, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; @@ -93,7 +93,7 @@ const PriorityEditForm = ({ data, onSave, onCancel }: PriorityEditFormProps): Re /> )} /> - {errors.name?.message} + {errors.name?.message} diff --git a/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx b/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx index 3bbe82494bcca..147dff65ad974 100644 --- a/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx +++ b/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx @@ -1,14 +1,23 @@ import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import Page from '../../../../client/components/Page'; +import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import { ResizeObserver } from './components/ResizeObserver'; import { AgentsSection, ChannelsSection, DepartmentsSection, StatusSection, TagsSection } from './sections'; const ReportsPage = () => { const t = useTranslation(); + const hasPermission = usePermission('view-livechat-reports'); + const isEnterprise = useHasLicenseModule('livechat-enterprise'); + + if (!hasPermission || !isEnterprise) { + return ; + } + return ( diff --git a/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx b/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx index 92a4d8d44199b..d885242f77221 100644 --- a/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx +++ b/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Box, Skeleton, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ComponentProps, ReactElement } from 'react'; import React from 'react'; @@ -61,7 +61,7 @@ export const ReportCard = ({ data-qa={id} aria-busy={isLoading} > - + @@ -76,9 +76,9 @@ export const ReportCard = ({ - - - + + + {isLoading && LoadingSkeleton} @@ -94,8 +94,8 @@ export const ReportCard = ({ {!isLoading && isDataFound && children} - - + +
); }; diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx index 30bcd110a56ed..83f2c4a36e72d 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx +++ b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx @@ -1,5 +1,5 @@ import type { IOmnichannelServiceLevelAgreements, Serialized } from '@rocket.chat/core-typings'; -import { Field, TextInput, Button, Margins, Box, NumberInput } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError, TextInput, Button, Margins, Box, NumberInput } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -78,32 +78,32 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE return ( - {t('Name')}* - + {t('Name')}* + - - {errors.name?.message} + + {errors.name?.message} - {t('Description')} - + {t('Description')} + - + - {t('Estimated_wait_time_in_minutes')}* - + {t('Estimated_wait_time_in_minutes')}* + - - {errors.dueTimeInMinutes?.message} + + {errors.dueTimeInMinutes?.message} - + {!isNew && ( @@ -116,7 +116,7 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE - + ); diff --git a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx similarity index 57% rename from apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js rename to apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index 086d796152fc4..f0baca991e2e6 100644 --- a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js +++ b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -1,3 +1,4 @@ +import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -7,9 +8,21 @@ import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; -const AutoCompleteTagMultiple = (props) => { - const { value, onlyMyTags = false, onChange = () => {}, department, viewAll = false } = props; +type AutoCompleteTagsMultipleProps = { + value?: PaginatedMultiSelectOption[]; + onlyMyTags?: boolean; + onChange?: (value: PaginatedMultiSelectOption[]) => void; + department?: string; + viewAll?: boolean; +}; +const AutoCompleteTagsMultiple = ({ + value = [], + onlyMyTags = false, + onChange = () => undefined, + department, + viewAll = false, +}: AutoCompleteTagsMultipleProps) => { const t = useTranslation(); const [tagsFilter, setTagsFilter] = useState(''); @@ -24,16 +37,10 @@ const AutoCompleteTagMultiple = (props) => { const { phase: tagsPhase, items: tagsItems, itemCount: tagsTotal } = useRecordList(tagsList); - const sortedByName = tagsItems.sort((a, b) => { - if (a.name > b.name) { - return 1; - } - if (a.name < b.name) { - return -1; - } - - return 0; - }); + const tagsOptions = useMemo(() => { + const pending = value.filter(({ value }) => !tagsItems.find((tag) => tag.value === value)); + return [...tagsItems, ...pending]; + }, [tagsItems, value]); return ( { onChange={onChange} filter={tagsFilter} setFilter={setTagsFilter} - options={sortedByName} + options={tagsOptions} width='100%' flexShrink={0} flexGrow={0} placeholder={t('Select_an_option')} - endReached={tagsPhase === AsyncStatePhase.LOADING ? () => {} : (start) => loadMoreTags(start, Math.min(50, tagsTotal))} + endReached={ + tagsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => start && loadMoreTags(start, Math.min(50, tagsTotal)) + } /> ); }; -export default memo(AutoCompleteTagMultiple); +export default memo(AutoCompleteTagsMultiple); diff --git a/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx b/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx index 61c1d11af9479..553d31b7479d8 100644 --- a/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx +++ b/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx @@ -3,7 +3,7 @@ import React from 'react'; import AutoCompleteTagsMultiple from './AutoCompleteTagsMultiple'; -type CurrentChatTagsProps = { value: Array; handler: () => void; department?: string; viewAll?: boolean }; +type CurrentChatTagsProps = { value: Array<{ value: string; label: string }>; handler: () => void; department?: string; viewAll?: boolean }; const CurrentChatTags: FC = ({ value, handler, department, viewAll }) => ( diff --git a/apps/meteor/ee/client/omnichannel/units/UnitEdit.js b/apps/meteor/ee/client/omnichannel/units/UnitEdit.js index 297a25d4a4c18..2c172bcf38c67 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitEdit.js +++ b/apps/meteor/ee/client/omnichannel/units/UnitEdit.js @@ -33,17 +33,6 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); - const departmentsSortedByName = departmentsItems.sort((a, b) => { - if (a.name > b.name) { - return 1; - } - if (a.name < b.name) { - return -1; - } - - return 0; - }); - const unit = data || {}; const currUnitMonitors = useMemo( @@ -82,6 +71,16 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r const { handleName, handleVisibility, handleDepartments, handleMonitors } = handlers; const { name, visibility, departments, monitors } = values; + const departmentsOptions = useMemo(() => { + const pending = departments.filter(({ value }) => !departmentsItems.find((dep) => dep.value === value)); + return [...departmentsItems, ...pending]; + }, [departments, departmentsItems]); + + const monitorsOptions = useMemo(() => { + const pending = monitors.filter(({ value }) => !monitorsItems.find((mon) => mon.value === value)); + return [...monitorsItems, ...pending]; + }, [monitors, monitorsItems]); + const nameError = useMemo(() => (!name || name.length === 0 ? t('The_field_is_required', t('name')) : undefined), [name, t]); const visibilityError = useMemo( () => (!visibility || visibility.length === 0 ? t('The_field_is_required', t('description')) : undefined), @@ -172,7 +171,7 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r withTitle filter={departmentsFilter} setFilter={setDepartmentsFilter} - options={departmentsSortedByName} + options={departmentsOptions} value={departments} error={hasUnsavedChanges && departmentError} maxWidth='100%' @@ -194,7 +193,7 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r withTitle filter={monitorsFilter} setFilter={setMonitorsFilter} - options={monitorsItems} + options={monitorsOptions} value={monitors} error={hasUnsavedChanges && unitMonitorsError} maxWidth='100%' diff --git a/apps/meteor/ee/client/startup/readReceipt.ts b/apps/meteor/ee/client/startup/readReceipt.ts index 0efc779a3b497..938cc4b6133ff 100644 --- a/apps/meteor/ee/client/startup/readReceipt.ts +++ b/apps/meteor/ee/client/startup/readReceipt.ts @@ -19,7 +19,7 @@ Meteor.startup(() => { id: 'receipt-detail', icon: 'info-circled', label: 'Info', - context: ['starred', 'message', 'message-mobile', 'threads'], + context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'duplication', action(_, props) { const { message = messageArgs(this).msg } = props; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx index 7c4965bce5d05..0fe32a90d8259 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx @@ -1,5 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle } from '@rocket.chat/ui-client'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; @@ -13,14 +13,14 @@ type EngagementDashboardCardProps = { const EngagementDashboardCard = ({ children, title = undefined }: EngagementDashboardCardProps): ReactElement => ( - {title && {title}} - - + {title && {title}} + + {children} - - + + ); diff --git a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx index ddcfe312122ff..b595dd9c1fae8 100644 --- a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx +++ b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx @@ -1,6 +1,6 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardFooter, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -27,9 +27,9 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => { return ( - {t('Seats_usage')} - - + {t('Seats_usage')} + + {!seatsCap ? ( @@ -43,15 +43,15 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => { /> )} - - - + + + - + ); }; diff --git a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts index eb029d91f537f..b4a45b49dda5e 100644 --- a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts +++ b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts @@ -8,6 +8,7 @@ export type SeatCapProps = { }; export const useSeatsCap = (): SeatCapProps | undefined => { + // #TODO: Stop using this endpoint const fetch = useEndpoint('GET', '/v1/licenses.maxActiveUsers'); const result = useQuery(['/v1/licenses.maxActiveUsers'], () => fetch()); diff --git a/apps/meteor/ee/client/views/audit/components/AuditForm.tsx b/apps/meteor/ee/client/views/audit/components/AuditForm.tsx index 3888013d87320..f8fbb63e39033 100644 --- a/apps/meteor/ee/client/views/audit/components/AuditForm.tsx +++ b/apps/meteor/ee/client/views/audit/components/AuditForm.tsx @@ -1,5 +1,5 @@ import type { IAuditLog } from '@rocket.chat/core-typings'; -import { Box, Field, TextInput, Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, FieldError, TextInput, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useController } from 'react-hook-form'; @@ -40,18 +40,18 @@ const AuditForm = ({ type, onSubmit }: AuditFormProps) => {
- {t('Message')} - + {t('Message')} + - + - {t('Date')} - + {t('Date')} + - {dateRangeFieldState.error?.type === 'required' && {t('The_field_is_required', t('Date'))}} - {dateRangeFieldState.error?.type === 'validate' && {dateRangeFieldState.error.message}} - + {dateRangeFieldState.error?.type === 'required' && {t('The_field_is_required', t('Date'))}} + {dateRangeFieldState.error?.type === 'validate' && {dateRangeFieldState.error.message}} + diff --git a/apps/meteor/ee/client/views/audit/components/AuditLogTable.tsx b/apps/meteor/ee/client/views/audit/components/AuditLogTable.tsx index 6125275c3972c..d51926fb574cb 100644 --- a/apps/meteor/ee/client/views/audit/components/AuditLogTable.tsx +++ b/apps/meteor/ee/client/views/audit/components/AuditLogTable.tsx @@ -1,4 +1,4 @@ -import { Field } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useTranslation, useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; @@ -55,10 +55,10 @@ const AuditLogTable = (): ReactElement => { return ( <> - {t('Date')} - + {t('Date')} + - + {isLoading && ( diff --git a/apps/meteor/ee/client/views/audit/components/tabs/DirectTab.tsx b/apps/meteor/ee/client/views/audit/components/tabs/DirectTab.tsx index 64472d0e1e91d..58facbb3bd421 100644 --- a/apps/meteor/ee/client/views/audit/components/tabs/DirectTab.tsx +++ b/apps/meteor/ee/client/views/audit/components/tabs/DirectTab.tsx @@ -1,4 +1,4 @@ -import { Field } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -30,17 +30,17 @@ const DirectTab = ({ form: { control } }: DirectTabProps): ReactElement => { return ( - {t('Users')} - + {t('Users')} + - - {usersFieldState.error?.type === 'required' && {t('The_field_is_required', t('Users'))}} - {usersFieldState.error?.type === 'validate' && {usersFieldState.error.message}} + + {usersFieldState.error?.type === 'required' && {t('The_field_is_required', t('Users'))}} + {usersFieldState.error?.type === 'validate' && {usersFieldState.error.message}} ); }; diff --git a/apps/meteor/ee/client/views/audit/components/tabs/OmnichannelTab.tsx b/apps/meteor/ee/client/views/audit/components/tabs/OmnichannelTab.tsx index 9d4bf2c45cbf5..772880fd6c218 100644 --- a/apps/meteor/ee/client/views/audit/components/tabs/OmnichannelTab.tsx +++ b/apps/meteor/ee/client/views/audit/components/tabs/OmnichannelTab.tsx @@ -1,4 +1,4 @@ -import { Field } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -30,21 +30,21 @@ const OmnichannelTab = ({ form: { control } }: OmnichannelTabProps): ReactElemen return ( <> - {t('Visitor')} - + {t('Visitor')} + - - {visitorFieldState.error?.type === 'required' && {t('The_field_is_required', t('Visitor'))}} - {visitorFieldState.error?.type === 'validate' && {visitorFieldState.error.message}} + + {visitorFieldState.error?.type === 'required' && {t('The_field_is_required', t('Visitor'))}} + {visitorFieldState.error?.type === 'validate' && {visitorFieldState.error.message}} - {t('Agent')} - + {t('Agent')} + { if (agentFieldState.error?.type === 'required') { @@ -57,9 +57,9 @@ const OmnichannelTab = ({ form: { control } }: OmnichannelTabProps): ReactElemen onChange={agentField.onChange} placeholder={t('Username_Placeholder')} /> - - {agentFieldState.error?.type === 'required' && {t('The_field_is_required', t('Agent'))}} - {agentFieldState.error?.type === 'validate' && {agentFieldState.error.message}} + + {agentFieldState.error?.type === 'required' && {t('The_field_is_required', t('Agent'))}} + {agentFieldState.error?.type === 'validate' && {agentFieldState.error.message}} ); diff --git a/apps/meteor/ee/client/views/audit/components/tabs/RoomsTab.tsx b/apps/meteor/ee/client/views/audit/components/tabs/RoomsTab.tsx index 551bd5bd5c0ee..984789bc0e01b 100644 --- a/apps/meteor/ee/client/views/audit/components/tabs/RoomsTab.tsx +++ b/apps/meteor/ee/client/views/audit/components/tabs/RoomsTab.tsx @@ -1,4 +1,4 @@ -import { Field } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -19,17 +19,17 @@ const RoomsTab = ({ form: { control } }: RoomsTabProps): ReactElement => { return ( - {t('Channel_name')} {/* TODO: should it be `Room_name`? */} - + {t('Channel_name')} {/* TODO: should it be `Room_name`? */} + - - {ridFieldState.error?.type === 'required' && {t('The_field_is_required', t('Channel_name'))}} - {ridFieldState.error?.type === 'validate' && {ridFieldState.error.message}} + + {ridFieldState.error?.type === 'required' && {t('The_field_is_required', t('Channel_name'))}} + {ridFieldState.error?.type === 'validate' && {ridFieldState.error.message}} ); }; diff --git a/apps/meteor/ee/client/views/audit/components/tabs/UsersTab.tsx b/apps/meteor/ee/client/views/audit/components/tabs/UsersTab.tsx index aeed1ae4aa8ce..75e769baa71d0 100644 --- a/apps/meteor/ee/client/views/audit/components/tabs/UsersTab.tsx +++ b/apps/meteor/ee/client/views/audit/components/tabs/UsersTab.tsx @@ -1,4 +1,4 @@ -import { Field } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -30,17 +30,17 @@ const UsersTab = ({ form: { control } }: UsersTabProps): ReactElement => { return ( - {t('Users')} - + {t('Users')} + - - {usersFieldState.error?.type === 'required' && {t('The_field_is_required', t('Users'))}} - {usersFieldState.error?.type === 'validate' && {usersFieldState.error.message}} + + {usersFieldState.error?.type === 'required' && {t('The_field_is_required', t('Users'))}} + {usersFieldState.error?.type === 'validate' && {usersFieldState.error.message}} ); }; diff --git a/apps/meteor/ee/client/voip/components/modals/WrapUpCallModal.tsx b/apps/meteor/ee/client/voip/components/modals/WrapUpCallModal.tsx index bb144fa750eb2..31aedb04aa105 100644 --- a/apps/meteor/ee/client/voip/components/modals/WrapUpCallModal.tsx +++ b/apps/meteor/ee/client/voip/components/modals/WrapUpCallModal.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Field, Modal, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, Button, Field, FieldLabel, FieldRow, FieldHint, Modal, TextAreaInput } from '@rocket.chat/fuselage'; import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; @@ -52,11 +52,11 @@ export const WrapUpCallModal = ({ closeRoom }: WrapUpCallModalProps): ReactEleme - {t('Notes')} - + {t('Notes')} + - - {t('These_notes_will_be_available_in_the_call_summary')} + + {t('These_notes_will_be_available_in_the_call_summary')} void} /> diff --git a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx index de409bf13f071..20d94e0b9e4b4 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx @@ -1,5 +1,5 @@ import { css } from '@rocket.chat/css-in-js'; -import { Field, Modal, IconButton } from '@rocket.chat/fuselage'; +import { Field, FieldError, Modal, IconButton } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -51,9 +51,9 @@ const DialPadModal = ({ initialValue, errorMessage: initialErrorMessage, handleC isButtonDisabled={isButtonDisabled} handleOnChange={handleOnChange} /> - + {inputError} - + diff --git a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx index 4bda542fdb63d..70202a42348bc 100644 --- a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx +++ b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Modal, Field, Select, Button, Box } from '@rocket.chat/fuselage'; +import { Modal, Field, FieldLabel, FieldRow, Select, Button, Box } from '@rocket.chat/fuselage'; import { useTranslation, useAvailableDevices, @@ -77,8 +77,8 @@ const DeviceSettingsModal = (): ReactElement => { )} - {t('Microphone')} - + {t('Microphone')} + { )} /> - + diff --git a/apps/meteor/ee/server/api/api.ts b/apps/meteor/ee/server/api/api.ts index f152a94ebcbf1..ee2049bb70ae4 100644 --- a/apps/meteor/ee/server/api/api.ts +++ b/apps/meteor/ee/server/api/api.ts @@ -1,7 +1,8 @@ +import { License } from '@rocket.chat/license'; + import { API } from '../../../app/api/server/api'; import type { NonEnterpriseTwoFactorOptions, Options } from '../../../app/api/server/definition'; import { use } from '../../../app/settings/server/Middleware'; -import { isEnterprise } from '../../app/license/server/license'; // Overwrites two factor method to enforce 2FA check for enterprise APIs when // no license was provided to prevent abuse on enterprise APIs. @@ -10,7 +11,7 @@ const isNonEnterpriseTwoFactorOptions = (options?: Options): options is NonEnter !!options && 'forceTwoFactorAuthenticationForNonEnterprise' in options && Boolean(options.forceTwoFactorAuthenticationForNonEnterprise); API.v1.processTwoFactor = use(API.v1.processTwoFactor, ([params, ...context], next) => { - if (isNonEnterpriseTwoFactorOptions(params.options) && !isEnterprise()) { + if (isNonEnterpriseTwoFactorOptions(params.options) && !License.hasValidLicense()) { const options: NonEnterpriseTwoFactorOptions = { ...params.options, twoFactorOptions: { diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 5d21b20f20381..2c8f8c5ca6055 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,8 +1,8 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; -import { hasLicense } from '../../app/license/server/license'; type GetMessageReadReceiptsProps = { messageId: IMessage['_id']; @@ -24,7 +24,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!hasLicense('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ab8d72164a97d..ff5c3fcc3e47f 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,17 +1,10 @@ +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; +import { isLicensesInfoProps } from '@rocket.chat/rest-typings'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import type { ILicense } from '../../app/license/definition/ILicense'; -import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; - -function licenseTransform(license: ILicense): ILicense { - return { - ...license, - modules: flatModules(license.modules), - }; -} API.v1.addRoute( 'licenses.get', @@ -22,15 +15,26 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const licenses = getLicenses() - .filter(({ valid }) => valid) - .map(({ license }) => licenseTransform(license)); + const license = License.getUnmodifiedLicenseAndModules(); + const licenses = license ? [license] : []; return API.v1.success({ licenses }); }, }, ); +API.v1.addRoute( + 'licenses.info', + { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, + { + async get() { + const data = await License.getInfo(Boolean(this.queryParams.loadValues)); + + return API.v1.success({ data }); + }, + }, +); + API.v1.addRoute( 'licenses.add', { authRequired: true }, @@ -45,7 +49,7 @@ API.v1.addRoute( } const { license } = this.bodyParams; - if (!validateFormat(license)) { + if (!(await License.validateFormat(license))) { return API.v1.failure('Invalid license'); } @@ -61,7 +65,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const maxActiveUsers = getMaxActiveUsers() || null; + const maxActiveUsers = License.getMaxActiveUsers() || null; const activeUsers = await Users.getActiveLocalUserCount(); return API.v1.success({ maxActiveUsers, activeUsers }); @@ -74,7 +78,7 @@ API.v1.addRoute( { authOrAnonRequired: true }, { get() { - const isEnterpriseEdtion = isEnterprise(); + const isEnterpriseEdtion = License.hasValidLicense(); return API.v1.success({ isEnterprise: isEnterpriseEdtion }); }, }, diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 712e7583b7096..c10c32c3ee1af 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -1,11 +1,11 @@ import type { IRole } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Roles } from '@rocket.chat/models'; import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; import { settings } from '../../../app/settings/server/index'; -import { isEnterprise } from '../../app/license/server'; import { insertRoleAsync } from '../lib/roles/insertRole'; import { updateRole } from '../lib/roles/updateRole'; @@ -96,7 +96,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } @@ -154,7 +154,7 @@ API.v1.addRoute( const role = await Roles.findOne(roleId); - if (!isEnterprise() && !role?.protected) { + if (!License.hasValidLicense() && !role?.protected) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index 41c30aba401bc..cdd454fd5beec 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -1,4 +1,5 @@ import type { IUser, ISession, DeviceManagementSession, DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users, Sessions } from '@rocket.chat/models'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -7,7 +8,6 @@ import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { getPaginationItems } from '../../../app/api/server/helpers/getPaginationItems'; import { Notifications } from '../../../app/notifications/server'; -import { hasLicense } from '../../app/license/server/license'; const ajv = new Ajv({ coerceTypes: true }); @@ -85,7 +85,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsPaginateProps }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -108,7 +108,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -127,7 +127,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async post() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -153,7 +153,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -193,7 +193,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -212,7 +212,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, { async post() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 96247e7045450..fc436b8229cfa 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,9 +1,9 @@ import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { License } from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; import type { SuccessResult } from '../../../../../app/api/server/definition'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import { getAppsConfig } from '../../../../app/license/server/license'; import type { AppsRestApi } from '../rest'; type AppsCountResult = { @@ -23,7 +23,7 @@ export const appsCountHandler = (apiManager: AppsRestApi) => const manager = apiManager._manager as AppManager; const apps = manager.get({ enabled: true }); - const { maxMarketplaceApps, maxPrivateApps } = getAppsConfig(); + const { maxMarketplaceApps, maxPrivateApps } = License.getAppsConfig(); return API.v1.success({ totalMarketplaceEnabled: apps.filter((app) => getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'marketplace') diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 1203d0d8c9119..f356f3e45a18c 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -3,6 +3,7 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; @@ -17,7 +18,7 @@ import { settings } from '../../../../app/settings/server'; import { Info } from '../../../../app/utils/rocketchat.info'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; -import { canEnableApp, isEnterprise } from '../../../app/license/server/license'; +import { canEnableApp } from '../../../app/license/server/canEnableApp'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; import { appEnableCheck } from '../marketplace/appEnableCheck'; import { notifyAppInstall } from '../marketplace/appInstall'; @@ -1149,7 +1150,7 @@ export class AppsRestApi { const storedApp = prl.getStorageItem(); const { installationSource, marketplaceInfo } = storedApp; - if (!isEnterprise() && installationSource === AppInstallationSource.MARKETPLACE) { + if (!License.hasValidLicense() && installationSource === AppInstallationSource.MARKETPLACE) { try { const baseUrl = orchestrator.getMarketplaceUrl() as string; const headers = getDefaultHeaders(); diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index c21508cbc6263..9e4d6f00e7f02 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -19,7 +19,7 @@ import { } from '../../../app/apps/server/converters'; import { AppThreadsConverter } from '../../../app/apps/server/converters/threads'; import { settings, settingsRegistry } from '../../../app/settings/server'; -import { canEnableApp } from '../../app/license/server/license'; +import { canEnableApp } from '../../app/license/server/canEnableApp'; import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index 40815e213b0c4..5f1a84557d70d 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -1,18 +1,18 @@ import type { IImportUser, ILDAPEntry, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; import type { LDAPConnection } from '../../../server/lib/ldap/Connection'; import { logger } from '../../../server/lib/ldap/Logger'; -import { onLicense } from '../../app/license/server'; import { LDAPEEManager } from '../lib/ldap/Manager'; import { LDAPEE } from '../sdk'; import { addSettings, ldapIntervalValuesToCronMap } from '../settings/ldap'; Meteor.startup(async () => { - await onLicense('ldap-enterprise', async () => { + await License.onLicense('ldap-enterprise', async () => { await addSettings(); // Configure background sync cronjob diff --git a/apps/meteor/ee/server/configuration/oauth.ts b/apps/meteor/ee/server/configuration/oauth.ts index 984670af60034..aa66a46caf699 100644 --- a/apps/meteor/ee/server/configuration/oauth.ts +++ b/apps/meteor/ee/server/configuration/oauth.ts @@ -1,11 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Roles } from '@rocket.chat/models'; import { capitalize } from '@rocket.chat/string-helpers'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { onLicense } from '../../app/license/server'; import { OAuthEEManager } from '../lib/oauth/Manager'; interface IOAuthUserService { @@ -54,7 +54,7 @@ function getChannelsMap(channelsMap: string): Record | undefined { } } -await onLicense('oauth-enterprise', () => { +await License.onLicense('oauth-enterprise', () => { callbacks.add('afterProcessOAuthUser', async (auth: IOAuthUserService) => { auth.serviceName = capitalize(auth.serviceName); const settings = getOAuthSettings(auth.serviceName); diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index cf36ddeb0cab9..67c8d79450300 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -1,11 +1,11 @@ import { Calendar } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/outlookCalendar'; Meteor.startup(() => - onLicense('outlook-calendar', async () => { + License.onLicense('outlook-calendar', async () => { addSettings(); await Calendar.setupNextNotification(); diff --git a/apps/meteor/ee/server/configuration/saml.ts b/apps/meteor/ee/server/configuration/saml.ts index 1e50fc7160b5c..96dca07829c64 100644 --- a/apps/meteor/ee/server/configuration/saml.ts +++ b/apps/meteor/ee/server/configuration/saml.ts @@ -1,13 +1,13 @@ +import { License } from '@rocket.chat/license'; import { Roles, Users } from '@rocket.chat/models'; import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; import { SAMLUtils } from '../../../app/meteor-accounts-saml/server/lib/Utils'; import { settings } from '../../../app/settings/server'; import { ensureArray } from '../../../lib/utils/arrayUtils'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/saml'; -await onLicense('saml-enterprise', () => { +await License.onLicense('saml-enterprise', () => { SAMLUtils.events.on('mapUser', async ({ profile, userObject }: { profile: Record; userObject: ISAMLUser }) => { const roleAttributeName = settings.get('SAML_Custom_Default_role_attribute_name') as string; const roleAttributeSync = settings.get('SAML_Custom_Default_role_attribute_sync'); @@ -67,4 +67,4 @@ await onLicense('saml-enterprise', () => { }); // For setting creation we add the listener first because the event is emmited during startup -SAMLUtils.events.on('addSettings', (name: string): void | Promise => onLicense('saml-enterprise', () => addSettings(name))); +SAMLUtils.events.on('addSettings', (name: string): void | Promise => License.onLicense('saml-enterprise', () => addSettings(name))); diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts index a9debed01b19a..035110904840c 100644 --- a/apps/meteor/ee/server/configuration/videoConference.ts +++ b/apps/meteor/ee/server/configuration/videoConference.ts @@ -1,16 +1,16 @@ import { VideoConf } from '@rocket.chat/core-services'; import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { videoConfTypes } from '../../../server/lib/videoConfTypes'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/video-conference'; Meteor.startup(async () => { - await onLicense('videoconference-enterprise', async () => { + await License.onLicense('videoconference-enterprise', async () => { await addSettings(); videoConfTypes.registerVideoConferenceType( diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 2e526776c7723..f5b385c9a8054 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -1,5 +1,3 @@ -import './startup'; - import '../app/license/server/index'; import '../app/api-enterprise/server/index'; import '../app/authorization/server/index'; @@ -8,7 +6,6 @@ import '../app/livechat-enterprise/server/index'; import '../app/message-read-receipt/server/index'; import '../app/voip-enterprise/server/index'; import '../app/settings/server/index'; -import '../app/teams-mention/server/index'; import './api'; import './requestSeatsRoute'; import './configuration/index'; diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/apps/meteor/ee/server/lib/EnterpriseCheck.ts index 8bccfed590715..ca8cd1e25b108 100644 --- a/apps/meteor/ee/server/lib/EnterpriseCheck.ts +++ b/apps/meteor/ee/server/lib/EnterpriseCheck.ts @@ -41,7 +41,7 @@ export const EnterpriseCheck: ServiceSchema = { async started(): Promise { setInterval(async () => { try { - const hasLicense = await this.broker.call('license.hasLicense', ['scalability']); + const hasLicense = await this.broker.call('license.hasValidLicense', ['scalability']); if (hasLicense) { checkFails = 0; return; diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index deb6cdcec666d..6c04574ad557a 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -1,6 +1,6 @@ import { Team } from '@rocket.chat/core-services'; import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser, IImportRecord } from '@rocket.chat/core-typings'; -import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; +import { Users, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import type ldapjs from 'ldapjs'; import type { @@ -271,10 +271,12 @@ export class LDAPEEManager extends LDAPManager { logger.debug(`Channel '${channel}' doesn't exist, creating it.`); const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || ''; - // #ToDo: Remove typecastings when createRoom is converted to ts. - const room = await createRoom('c', channel, roomOwner, [], false, false, { + + const user = await Users.findOneByUsernameIgnoringCase(roomOwner); + + const room = await createRoom('c', channel, user, [], false, false, { customFields: { ldap: true }, - } as any); + }); if (!room?.rid) { logger.error(`Unable to auto-create channel '${channel}' during ldap sync.`); return; @@ -574,7 +576,7 @@ export class LDAPEEManager extends LDAPManager { } private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise { - const users = await UsersRaw.findLDAPUsers().toArray(); + const users = await Users.findLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); @@ -586,7 +588,7 @@ export class LDAPEEManager extends LDAPManager { } private static async updateUserAvatars(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findLDAPUsers().toArray(); + const users = await Users.findLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); if (!ldapUser) { @@ -615,7 +617,7 @@ export class LDAPEEManager extends LDAPManager { } private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findConnectedLDAPUsers().toArray(); + const users = await Users.findConnectedLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); @@ -624,7 +626,7 @@ export class LDAPEEManager extends LDAPManager { } if (this.isUserDeactivated(ldapUser)) { - await UsersRaw.unsetLoginTokens(user._id); + await Users.unsetLoginTokens(user._id); } } } diff --git a/apps/meteor/ee/server/lib/oauth/Manager.ts b/apps/meteor/ee/server/lib/oauth/Manager.ts index b24d7436a7841..b75c8aa9a7a5e 100644 --- a/apps/meteor/ee/server/lib/oauth/Manager.ts +++ b/apps/meteor/ee/server/lib/oauth/Manager.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { Roles, Rooms } from '@rocket.chat/models'; +import { Roles, Rooms, Users } from '@rocket.chat/models'; import { addUserToRoom } from '../../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../app/lib/server/functions/createRoom'; @@ -20,6 +20,12 @@ export class OAuthEEManager { if (channelsMap && user && identity && groupClaimName) { const groupsFromSSO = identity[groupClaimName] || []; + const userChannelAdmin = await Users.findOneByUsernameIgnoringCase(channelsAdmin); + if (!userChannelAdmin) { + logger.error(`could not create channel, user not found: ${channelsAdmin}`); + return; + } + for await (const ssoGroup of Object.keys(channelsMap)) { if (typeof ssoGroup === 'string') { let channels = channelsMap[ssoGroup]; @@ -30,7 +36,7 @@ export class OAuthEEManager { const name = await getValidRoomName(channel.trim(), undefined, { allowDuplicates: true }); let room = await Rooms.findOneByNonValidatedName(name); if (!room) { - const createdRoom = await createRoom('c', channel, channelsAdmin, [], false, false); + const createdRoom = await createRoom('c', channel, userChannelAdmin, [], false, false); if (!createdRoom?.rid) { logger.error(`could not create channel ${channel}`); return; diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index e38f9de3c3101..f3a380a8f2280 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, IRole, AtLeast } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; import { addUserRolesAsync } from '../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../server/lib/roles/removeUserFromRoles'; -import { canAddNewUser } from '../../app/license/server/license'; type setUserRolesOptions = { // If specified, the function will not add nor remove any role that is not on this list. @@ -72,7 +72,7 @@ export async function syncUserRoles( } const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest'; - if (wasGuest && !(await canAddNewUser())) { + if (wasGuest && (await License.shouldPreventAction('activeUsers'))) { throw new Error('error-license-user-limit-reached'); } diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index 0fc4fd33a9b16..5bb755ee347f5 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -96,6 +96,12 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe events: { broadcast(ctx: any) { const { eventName, streamName, args } = ctx.params; + const { nodeID } = ctx; + + const fromLocalNode = nodeID === InstanceStatus.id(); + if (fromLocalNode) { + return; + } const instance = StreamerCentral.instances[streamName]; if (!instance) { @@ -137,7 +143,7 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe await InstanceStatus.registerInstance('rocket.chat', instance); - const hasLicense = await License.hasLicense('scalability'); + const hasLicense = await License.hasModule('scalability'); if (!hasLicense) { return; } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.ts b/apps/meteor/ee/server/methods/getReadReceipts.ts index a30eec300c41b..78fe8a4d967e5 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.ts +++ b/apps/meteor/ee/server/methods/getReadReceipts.ts @@ -1,11 +1,11 @@ import type { ReadReceipt as ReadReceiptType, IMessage } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../app/authorization/server/functions/canAccessRoom'; -import { hasLicense } from '../../app/license/server/license'; import { ReadReceipt } from '../lib/message-read-receipt/ReadReceipt'; declare module '@rocket.chat/ui-contexts' { @@ -17,7 +17,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async getReadReceipts({ messageId }) { - if (!hasLicense('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' }); } diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index 227da1c98e8c7..3295af1b61794 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -11,8 +11,6 @@ import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, F import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { LivechatRoomsRaw } from '../../../../server/models/raw/LivechatRooms'; -import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; -import { addQueryRestrictionsToRoomsModel } from '../../../app/livechat-enterprise/server/lib/query.helper'; declare module '@rocket.chat/model-typings' { interface ILivechatRoomsModel { @@ -271,25 +269,14 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo ], }; const update = { $set: { departmentAncestors: [unitId] } }; - queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, query, update }); - const associationResult = await this.updateMany(query, update); - queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, result: associationResult }); + await this.updateMany(query, update); const queryToDisassociateOldRoomsConnectedToUnit = { departmentAncestors: unitId, departmentId: { $nin: departments }, }; const updateToDisassociateRooms = { $unset: { departmentAncestors: 1 } }; - queriesLogger.debug({ - msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, - query: queryToDisassociateOldRoomsConnectedToUnit, - update: updateToDisassociateRooms, - }); - const disassociationResult = await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms); - queriesLogger.debug({ - msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, - result: disassociationResult, - }); + await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms); } async removeUnitAssociationFromRooms(unitId: string): Promise { @@ -297,9 +284,7 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo departmentAncestors: unitId, }; const update = { $unset: { departmentAncestors: 1 } }; - queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, query, update }); - const result = await this.updateMany(query, update); - queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, result }); + await this.updateMany(query, update); } async updateDepartmentAncestorsById(rid: string, departmentAncestors?: string[]) { @@ -310,35 +295,6 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return this.updateOne(query, update); } - /** @deprecated Use updateOne or updateMany instead */ - async update(...args: Parameters) { - const [query, ...restArgs] = args; - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.update', query: restrictedQuery }); - return super.update(restrictedQuery, ...restArgs); - } - - async updateOne(...args: [...Parameters, { bypassUnits?: boolean }?]) { - const [query, update, opts, extraOpts] = args; - if (extraOpts?.bypassUnits) { - // When calling updateOne from a service, we cannot call the meteor code inside the query restrictions - // So the solution now is to pass a bypassUnits flag to the updateOne method which prevents checking - // units restrictions on the query, but just for the query the service is actually using - // We need to find a way of remove the meteor dependency when fetching units, and then, we can remove this flag - return super.updateOne(query, update, opts); - } - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); - return super.updateOne(restrictedQuery, update, opts); - } - - async updateMany(...args: Parameters) { - const [query, ...restArgs] = args; - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateMany', query: restrictedQuery }); - return super.updateMany(restrictedQuery, ...restArgs); - } - getConversationsBySource(start: Date, end: Date, extraQuery: Filter): AggregationCursor { return this.col.aggregate( [ @@ -357,7 +313,10 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo }, { $group: { - _id: '$source', + _id: { + type: '$source.type', + alias: '$source.alias', + }, value: { $sum: 1 }, }, }, @@ -540,6 +499,13 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo }, }, }, + { + $match: { + _id: { + $ne: null, + }, + }, + }, { $sort: sort || { total: 1 }, }, diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index b49cbb959df1e..fcabf12fa4f81 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -4,7 +4,6 @@ import { LivechatUnitMonitors, LivechatDepartment, LivechatRooms } from '@rocket import type { FindOptions, Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; -import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; import { getUnitsFromUser } from '../../../app/livechat-enterprise/server/lib/units'; const addQueryRestrictions = async (originalQuery: Filter = {}) => { @@ -40,7 +39,6 @@ export class LivechatUnitRaw extends BaseRaw implement options: FindOptions, ): Promise> { const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.find', query }); return this.col.find(query, options) as FindCursor; } @@ -50,20 +48,9 @@ export class LivechatUnitRaw extends BaseRaw implement options: FindOptions, ): Promise { const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.findOne', query }); return this.col.findOne(query, options); } - async update( - originalQuery: Filter, - update: Filter, - options: FindOptions, - ): Promise { - const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.update', query }); - return this.col.updateOne(query, update, options); - } - remove(query: Filter): Promise { return this.deleteMany(query); } diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 580f4c025e078..4fd8433358ca3 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../app/license/server/license'; +import { License } from '@rocket.chat/license'; // To facilitate our lives with the stream // Collection will be registered on CE too @@ -8,7 +8,7 @@ import('./OmnichannelServiceLevelAgreements'); import('./AuditLog'); import('./ReadReceipts'); -await onLicense('livechat-enterprise', () => { +await License.onLicense('livechat-enterprise', () => { import('./CannedResponse'); import('./LivechatTag'); import('./LivechatUnit'); diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index c9759c95b0af5..77b48a0d1f92c 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,155 @@ # rocketchat-services +## 1.1.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/models@0.0.15 + +## 1.1.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 1.1.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 1.1.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 + +## 1.1.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 1.1.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 1.1.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 1.1.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 1.1.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 1.1.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + +## 1.1.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + ## 1.1.3 ### Patch Changes diff --git a/apps/meteor/ee/server/services/Dockerfile b/apps/meteor/ee/server/services/Dockerfile index 6b55a5c35acac..e1a1074fcb605 100644 --- a/apps/meteor/ee/server/services/Dockerfile +++ b/apps/meteor/ee/server/services/Dockerfile @@ -10,6 +10,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./packages/core-services packages/core-services COPY ./packages/core-typings packages/core-typings @@ -56,6 +57,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./apps/meteor/ee/server/services/package.json ./apps/meteor/ee/server/services/package.json ENV NODE_ENV=production \ diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 5ef03a985d0f4..ea6660730c638 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.1.3", + "version": "1.1.9", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { @@ -27,7 +27,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "ajv": "^8.11.0", "bcrypt": "^5.0.1", "body-parser": "^1.20.2", @@ -41,7 +41,7 @@ "jaeger-client": "^3.19.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.6.1", "pino": "^8.15.0", "sodium-native": "^3.3.0", @@ -50,7 +50,7 @@ "ws": "^8.8.1" }, "devDependencies": { - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "^0.32.0", "@types/cookie": "^0.5.1", "@types/cookie-parser": "^1.4.3", "@types/ejson": "^2.2.0", diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index 1c214ba0a406b..eec50e91b7dd5 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,10 +1,10 @@ +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onInvalidateLicense } from '../../../app/license/server/license'; import { Apps } from '../../apps'; Meteor.startup(() => { - onInvalidateLicense(() => { + License.onInvalidateLicense(() => { void Apps.disableApps(); }); }); diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 441429e51b229..c38794a7582ec 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -1,7 +1,8 @@ -import { onLicense } from '../../app/license/server'; +import { License } from '@rocket.chat/license'; + import { createPermissions } from '../lib/audit/startup'; -await onLicense('auditing', async () => { +await License.onLicense('auditing', async () => { await import('../lib/audit/methods'); await createPermissions(); diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index a9a1c805f72d5..2ad5fd3b8a4fa 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,8 +1,9 @@ -import { onToggledFeature } from '../../app/license/server/license'; +import { License } from '@rocket.chat/license'; + import { addSettings } from '../settings/deviceManagement'; let stopListening: (() => void) | undefined; -onToggledFeature('device-management', { +License.onToggledFeature('device-management', { up: async () => { const { createPermissions, createEmailTemplates } = await import('../lib/deviceManagement/startup'); const { listenSessionLogin } = await import('../lib/deviceManagement/session'); diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index 2fc393379bf3f..ca5dda577bb07 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -1,8 +1,7 @@ +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onToggledFeature } from '../../app/license/server/license'; - -onToggledFeature('engagement-dashboard', { +License.onToggledFeature('engagement-dashboard', { up: () => Meteor.startup(async () => { const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup'); diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index f4e2452ec8060..bfcb1ba5fa8b6 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,17 +1,14 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; -import { getMaxRoomsPerGuest } from '../../app/license/server/license'; callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - const totalSubscriptions = await Subscriptions.countByUserId(user._id); - - if (totalSubscriptions >= getMaxRoomsPerGuest()) { + if (await License.shouldPreventAction('roomsPerGuest', 0, { userId: user._id })) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index b390539ad6b11..e72852052acc3 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { throttle } from 'underscore'; @@ -6,7 +7,6 @@ import { throttle } from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { validateUserRoles } from '../../app/authorization/server/validateUserRoles'; -import { canAddNewUser, getMaxActiveUsers, onValidateLicenses } from '../../app/license/server/license'; import { createSeatsLimitBanners, disableDangerBannerDiscardingDismissal, @@ -22,7 +22,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (!(await canAddNewUser(userCount))) { + if (await License.shouldPreventAction('activeUsers', userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -52,7 +52,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -62,37 +62,13 @@ callbacks.add( callbacks.add( 'validateUserRoles', - async (userData: Partial) => { - const isGuest = userData.roles?.includes('guest'); - if (isGuest) { - await validateUserRoles(Meteor.userId(), userData); - return; - } - - if (!userData._id) { - return; - } - - const currentUserData = await Users.findOneById(userData._id); - if (currentUserData?.type === 'app') { - return; - } - - const wasGuest = currentUserData?.roles?.length === 1 && currentUserData.roles.includes('guest'); - if (!wasGuest) { - return; - } - - if (!(await canAddNewUser())) { - throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); - } - }, + async (userData: Partial) => validateUserRoles(userData), callbacks.priority.MEDIUM, 'check-max-user-seats', ); const handleMaxSeatsBanners = throttle(async function _handleMaxSeatsBanners() { - const maxActiveUsers = getMaxActiveUsers(); + const maxActiveUsers = License.getMaxActiveUsers(); if (!maxActiveUsers) { await disableWarningBannerDiscardingDismissal(); @@ -137,5 +113,5 @@ Meteor.startup(async () => { await handleMaxSeatsBanners(); - onValidateLicenses(handleMaxSeatsBanners); + License.onValidateLicense(handleMaxSeatsBanners); }); diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 5288b9a8e10e6..37aec21bfe565 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,8 +1,8 @@ import { api } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; -import { isEnterprise, onLicense } from '../../app/license/server'; import { LicenseService } from '../../app/license/server/license.internalService'; import { OmnichannelEE } from '../../app/livechat-enterprise/server/services/omnichannel.internalService'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; @@ -26,13 +26,13 @@ if (!isRunningMs()) { let federationService: FederationService; void (async () => { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { federationService = await FederationService.createFederationService(); api.registerService(federationService); } })(); -await onLicense('federation', async () => { +await License.onLicense('federation', async () => { const federationServiceEE = new FederationServiceEE(); if (federationService) { api.destroyService(federationService); diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index c9e4c513276c7..b31bf06350608 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,20 +1,13 @@ +import { License } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { onValidateLicenses, getLicenses } from '../../app/license/server/license'; - const handleHadTrial = (): void => { - getLicenses().forEach(({ valid, license }): void => { - if (!valid) { - return; - } - - if (license.meta?.trial) { - void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); - } - }); + if (License.getLicense()?.information.trial) { + void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + } }; Meteor.startup(() => { - onValidateLicenses(handleHadTrial); + License.onValidateLicense(handleHadTrial); }); diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index 0af2cee0c3775..8ac29d1915767 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,6 +1,5 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; - -import type { LicenseAppSources } from '../../ee/app/license/definition/ILicense'; +import type { LicenseAppSources } from '@rocket.chat/license'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 49ae28c5535da..0252f8fd61d8a 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -1,5 +1,3 @@ -import type { UrlWithParsedQuery } from 'url'; - import type { IMessage, IRoom, @@ -10,7 +8,6 @@ import type { ILivechatInquiryRecord, ILivechatVisitor, VideoConference, - ParsedUrl, OEmbedMeta, OEmbedUrlContent, Username, @@ -21,6 +18,7 @@ import type { ILivechatTagRecord, TransferData, AtLeast, + UserStatus, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -55,12 +53,12 @@ interface EventLikeCallbackSignatures { 'livechat.saveRoom': (room: IRoom) => void; 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; - 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; + 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: UserStatus }) => void; 'livechat.onNewAgentCreated': (agentId: string) => void; 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick }) => void; 'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void; - 'beforeAddedToRoom': (params: { user: IUser; inviter: IUser }) => void; + 'beforeAddedToRoom': (params: { user: AtLeast; inviter: IUser }) => void; 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void; 'beforeDeleteRoom': (params: IRoom) => void; 'beforeJoinDefaultChannels': (user: IUser) => void; @@ -168,24 +166,13 @@ type ChainedCallbackSignatures = { BusinessHourBehaviorClass: { new (): IBusinessHourBehavior }; }; 'renderMessage': (message: T) => T; - 'oembed:beforeGetUrlContent': (data: { - urlObj: Omit & { host?: unknown; search?: unknown }; - parsedUrl: ParsedUrl; - }) => { - urlObj: UrlWithParsedQuery; - parsedUrl: ParsedUrl; + 'oembed:beforeGetUrlContent': (data: { urlObj: URL }) => { + urlObj: URL; }; - 'oembed:afterParseContent': (data: { - url: string; - meta: OEmbedMeta; - headers: { [k: string]: string }; - parsedUrl: ParsedUrl; - content: OEmbedUrlContent; - }) => { + 'oembed:afterParseContent': (data: { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; content: OEmbedUrlContent }) => { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; }; 'livechat.beforeListTags': () => ILivechatTag[]; diff --git a/apps/meteor/lib/callbacks/callbacksBase.ts b/apps/meteor/lib/callbacks/callbacksBase.ts index e6681df783213..405cc5da80e66 100644 --- a/apps/meteor/lib/callbacks/callbacksBase.ts +++ b/apps/meteor/lib/callbacks/callbacksBase.ts @@ -170,7 +170,7 @@ export class Callbacks< this.setCallbacks(hook, hooks); } - run(hook: Hook, ...args: Parameters): void; + run(hook: Hook, ...args: Parameters): Promise; run( hook: Hook, diff --git a/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts new file mode 100644 index 0000000000000..4cea63a01f09c --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceAccessError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceAccessError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts new file mode 100644 index 0000000000000..8b4edcf8f5885 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceConnectionError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceConnectionError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceError.ts b/apps/meteor/lib/errors/CloudWorkspaceError.ts new file mode 100644 index 0000000000000..d843c42ea5207 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceError.ts @@ -0,0 +1,6 @@ +export class CloudWorkspaceError extends Error { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts new file mode 100644 index 0000000000000..96c9a28be82c0 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceLicenseError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceLicenseError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts new file mode 100644 index 0000000000000..aecec757aceeb --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceRegistrationError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceRegistrationError.name; + } +} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 7b0ea3b73cc74..3ee3366f47dd4 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.4.0-develop", + "version": "6.5.0-develop", "private": true, "author": { "name": "Rocket.Chat", @@ -30,7 +30,7 @@ "obj:dev": "TEST_MODE=true yarn dev", "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", "stylelint:fix": "stylelint --fix \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", - "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=4098\" tsc -p tsconfig.typecheck.json", + "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc -p tsconfig.typecheck.json", "deploy": "npm run build && pm2 startOrRestart pm2.json", "coverage": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' nyc -r html mocha --config ./.mocharc.js", "test:e2e": "playwright test", @@ -236,32 +236,36 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/instance-status": "workspace:^", + "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "next", + "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", - "@rocket.chat/logo": "next", + "@rocket.chat/logo": "^0.31.27", "@rocket.chat/memo": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", "@rocket.chat/omnichannel-services": "workspace:^", - "@rocket.chat/onboarding-ui": "next", + "@rocket.chat/onboarding-ui": "^0.32.1", + "@rocket.chat/password-policies": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/poplib": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/random": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/server-cloud-communication": "workspace:^", "@rocket.chat/server-fetch": "workspace:^", "@rocket.chat/sha256": "workspace:^", "@rocket.chat/string-helpers": "next", @@ -269,7 +273,7 @@ "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-composer": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "@rocket.chat/ui-theming": "workspace:^", "@rocket.chat/ui-video-conf": "workspace:^", "@rocket.chat/web-ui-registration": "workspace:^", @@ -285,7 +289,7 @@ "@xmldom/xmldom": "^0.8.8", "adm-zip": "0.5.10", "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", + "ajv-formats": "~2.1.1", "apn": "2.2.0", "archiver": "^3.1.1", "asterisk-manager": "^0.2.0", @@ -346,6 +350,7 @@ "imap": "^0.8.19", "ip-range-check": "^0.2.0", "is-svg": "^4.3.2", + "isolated-vm": "4.4.2", "jquery": "^3.6.0", "jschardet": "^3.0.0", "jsdom": "^16.7.0", @@ -370,8 +375,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.43", "mongo-message-queue": "^1.0.0", - "mongodb": "^4.12.1", - "mongodb-memory-server": "^7.6.3", + "mongodb": "^4.17.1", "nats": "^2.6.1", "node-abort-controller": "^3.1.1", "node-dogstatsd": "^0.0.7", @@ -413,6 +417,7 @@ "stream-buffers": "^3.0.2", "strict-uri-encode": "^2.0.0", "string-strip-html": "^7.0.3", + "suretype": "~2.4.1", "tar-stream": "^1.6.2", "textarea-caret": "^3.1.0", "tinykeys": "^1.4.0", @@ -468,5 +473,11 @@ }, "installConfig": { "hoistingLimits": "workspaces" + }, + "rocketchat": { + "minimumClientVersions": { + "desktop": "3.9.6", + "mobile": "4.39.0" + } } } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index d6b7e5a0739bd..392b4a99b3adb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -4299,10 +4299,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "يمنع هذا الإعداد كل المثيلات من إرسال تغييرات الحالة للمستخدمين إلى عملائهم مع الاحتفاظ بحالة تواجد كل المستخدمين من التحميل الأول!", "Troubleshoot_Disable_Sessions_Monitor": "تعطيل شاشة مراقبة الجلسات", "Troubleshoot_Disable_Sessions_Monitor_Alert": "يوقف هذا الإعداد معالجة جلسات المستخدم، ما يتسبب في توقف الإحصاءات عن العمل بشكل صحيح!", - "Troubleshoot_Disable_Statistics_Generator": "تعطيل منشئ الإحصاءات", - "Troubleshoot_Disable_Statistics_Generator_Alert": "يوقف هذا الإعداد معالجة كل الإحصاءات، ما يجعل صفحة المعلومات قديمة حتى ينقر شخص ما على زر التحديث وقد يتسبب في فقد معلومات أخرى حول النظام!", - "Troubleshoot_Disable_Workspace_Sync": "تعطيل مزامنة مساحة العمل", - "Troubleshoot_Disable_Workspace_Sync_Alert": "يوقف هذا الإعداد مزامنة هذا الخادم مع سحابة Rocket.Chat وقد يتسبب في حدوث مشاكل مع تراخيص السوق والمؤسسة!", "True": "صحيح", "Try_now": "المحاولة الآن", "Try_searching_in_the_marketplace_instead": "محاولة البحث في السوق بدلاً من ذلك", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "جرب أفضل خطة إصدار Enterprise لمدة 30 يومًا مجانًا", "onboarding.page.magicLinkEmail.title": "أرسلنا لك رابط تسجيل الدخول عبر البريد الإلكتروني", "onboarding.page.magicLinkEmail.subtitle": "انقر فوق الرابط الموجود في البريد الإلكتروني الذي أرسلناه لك للتو لتسجيل الدخول إلى مساحة العمل الخاصة بك. <1>ستنتهي صلاحية الرابط خلال 30 دقيقة.", - "onboarding.page.organizationInfoPage.title": "بعض التفاصيل الإضافية...", - "onboarding.page.organizationInfoPage.subtitle": "ستساعدنا هذه على تخصيص مساحة العمل الخاصة بك.", "onboarding.form.adminInfoForm.title": "معلومات المسؤول", "onboarding.form.adminInfoForm.subtitle": "نحتاج إلى هذا لإنشاء ملف شخصي مسؤول داخل مساحة العمل الخاصة بك", "onboarding.form.adminInfoForm.fields.fullName.label": "الاسم الكامل", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "التكامل مع مقدمي الخدمات الخارجيين (WhatsApp وFacebook وTelegram وTwitter)", "onboarding.form.registeredServerForm.included.apps": "الوصول إلى تطبيقات السوق", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "البريد الإلكتروني لحساب السحابة", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "لتسجيل الخادم الخاص بك، نحتاج إلى توصيله بحسابك السحابي. إذا كان لديك حساب سابقًا، فسنقوم بربطه تلقائيًا. وإن لم يكن لديك، فسيتم إنشاء حساب جديد", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "يرجى إدخال بريدك الإلكتروني", "onboarding.form.registeredServerForm.keepInformed": "أبقني على اطلاع بالأخبار والأحداث", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "يعني التسجيل موافقتي على تلقي تحديثات المنتج والأمان ذات الصلة", "onboarding.form.standaloneServerForm.title": "تأكيد الخادم المستقل", "onboarding.form.standaloneServerForm.servicesUnavailable": "لن تكون بعض الخدمات متاحة أو ستتطلب إعدادًا يدويًا", "onboarding.form.standaloneServerForm.publishOwnApp": "لإرسال الإشعارات، تحتاج إلى تجميع تطبيقك الخاص ونشره على Google Play وApp Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index b6e15bbf6f66c..9f0ef2e27a74e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -4227,10 +4227,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Aquesta configuració evita que totes les instàncies enviïn els canvis d'estat dels usuaris als clients, mantenint tots els usuaris amb el seu estat de presència des de la primera càrrega!", "Troubleshoot_Disable_Sessions_Monitor": "Desactiva el monitor de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Aquesta configuració deté el processament de les sessions de visita de l'LiveChat causant que les estadístiques deixin de funcionar!", - "Troubleshoot_Disable_Statistics_Generator": "Desactivar el generador d'estadístiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Aquest ajust deté el processament de totes les estadístiques fent que la pàgina d'informació quedi desactualitzada fins que algú faci clic al botó d'actualització i pot causar que falti altra informació en el sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desactiva la sincronització de l'espai de treball", - "Troubleshoot_Disable_Workspace_Sync_Alert": "¡Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede causar problemas con el mercado y las licencias de las empresas!", "True": "Sí", "Try_now": "Prova-ho ara", "Tuesday": "dimarts", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index ff081e3a5eeb4..c2fbddd26d295 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -3561,10 +3561,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Toto nastavení zakáže všem instancím odesílat změny stavu uživatelů a ponechat si nastavení při prvním načtení", "Troubleshoot_Disable_Sessions_Monitor": "Zakázat monitor sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Toto nastavení přestane zpracovávat uživatelské sessions a statistiky tak přestanou správně fungovat!", - "Troubleshoot_Disable_Statistics_Generator": "Zakázat generování statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Toto nastavení přestane zpracovávat statistiky, takže stránka s informacemi zůstane neaktuální dokud někdo nevynutí aktualizaci. Způsobuje neaktuálnost dat napříč systémem!", - "Troubleshoot_Disable_Workspace_Sync": "Zakázat synchronizaci pracovního prostoru", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Toto nastavení zakáže synchronizaci s Rocket.chat cloud a může způsobit problémy s marketplace a enterprise licencemi!", "True": "Ano", "Try_now": "Zkusit nyní", "Tuesday": "Úterý", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 317faac59164d..66e3bb1f035e8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -3582,10 +3582,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Denne indstilling forhindrer alle instancer fra at sende statusændringerne for brugerne til deres klienter, hvilket gør, at alle brugere vil have deres status vedr. tilstedeværelse fra de blev loadet i starten!", "Troubleshoot_Disable_Sessions_Monitor": "Deaktivér sessions-monitor", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Denne indstilling stopper behandlingen af brugersessioner og får statistikkerne til at stoppe med at virke korrekt!", - "Troubleshoot_Disable_Statistics_Generator": "Deaktivér statistik-generator", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Denne indstilling stopper behandlingen af alle statistikker, hvilket gør at informationssiden forældes, indtil nogen klikker på opdateringsknappen og kan også forårsage andre manglende oplysninger rundt omkring i systemet!", - "Troubleshoot_Disable_Workspace_Sync": "Deaktivér synkronisering af Workspace", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Denne indstilling stopper synkroniseringen af denne server med Rocket.Chat's cloud og kan forårsage problemer med marketplace og enteprise-licenser!", "True": "Sandt", "Try_now": "Forsøg nu", "Tuesday": "tirsdag", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 47649dd17b2de..52054a3ebdad1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -4821,10 +4821,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Diese Einstellung sorgt dafür, dass keine Instanz mehr die Statusänderungen der Benutzer an ihre Clients sendet, sodass die Benutzer den Präsenzstatus behalten, den sie beim ersten Laden hatten!", "Troubleshoot_Disable_Sessions_Monitor": "Sitzungsmonitor deaktivieren", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Diese Einstellung stoppt die Verarbeitung von Benutzersitzungen, was dazu führt, dass die Statistiken nicht mehr ordnungsgemäß funktionieren!", - "Troubleshoot_Disable_Statistics_Generator": "Statistikgenerator deaktivieren", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Diese Einstellung stoppt die Verarbeitung der gesamten Statistik, sodass die Infoseite so lange veraltet ist, bis jemand die Aktualisierungschaltfläche anklickt. Außerdem kann es sein, dass andere Systeminformationen fehlen!", - "Troubleshoot_Disable_Workspace_Sync": "Arbeitsbereichsynchronisierung deaktivieren", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Diese Einstellung stoppt die Synchronisierung des Servers mit der Rocket.Chat-Cloud und kann Probleme mit Marktplatz- und Unternehmenslizenzen verursachen!", "True": "Ja", "Try_now": "Jetzt versuchen", "Try_searching_in_the_marketplace_instead": "Versuchen Sie stattdessen den Marktplatz zu durchsuchen", @@ -4953,7 +4949,7 @@ "User__username__unmuted_in_room__roomName__": "Stummschaltung von Benutzer {{username}} in Raum {{roomName}} aufgehoben", "User_added": "Benutzer hinzugefügt", "User_added_by": "Der Benutzer {{user_added}} wurde von {{user_by}} hinzugefügt", - "User_added_to": "__user_added_ hinzugefügt", + "User_added_to": "hinzugefügt {{user_added}}", "User_added_successfully": "Benutzer erfolgreich hinzugefügt", "User_and_group_mentions_only": "Nur Benutzer- und Gruppenerwähnungen", "User_cant_be_empty": "Benutzer darf nicht leer sein", @@ -5466,8 +5462,6 @@ "onboarding.page.requestTrial.subtitle": "Testen Sie unseren besten Enterprise Edition-Plan 30 Tage lang gratis", "onboarding.page.magicLinkEmail.title": "Wir haben Ihnen einen Anmeldelink gesendet", "onboarding.page.magicLinkEmail.subtitle": "Klicken Sie auf den Link, in der gerade an Sie versandten E-Mail, um sich bei Ihrem Arbeitsbereich anzumelden. <1>Der Link verfällt in 30 Minuten.", - "onboarding.page.organizationInfoPage.title": "Ein paar zusätzliche Details...", - "onboarding.page.organizationInfoPage.subtitle": "Diese helfen uns, Ihren Arbeitsbereich zu personalisieren.", "onboarding.form.adminInfoForm.title": "Admin-Info", "onboarding.form.adminInfoForm.subtitle": "Das ist erforderlich, um ein Admin-Profil in Ihrem Arbeitsbereich zu erstellen", "onboarding.form.adminInfoForm.fields.fullName.label": "Vollständiger Name", @@ -5496,10 +5490,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integration mit externen Anbietern (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Zugriff auf Marktplatz-Apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-Konto-E-Mail", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Um Ihren Server zu registrieren, müssen wir ihn mit Ihrem Cloud-Konto verbinden. Wenn Sie bereits eines haben, werden wir es automatisch verknüpfen. Andernfalls wird ein neues Konto erstellt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein", "onboarding.form.registeredServerForm.keepInformed": "Informieren Sie mich über Neuigkeiten und Ereignisse", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Mit der Registrierung stimme ich zu, dass ich relevante Produkt- und Sicherheits-Updates erhalte", "onboarding.form.standaloneServerForm.title": "Stand-alone-Server-Bestätigung", "onboarding.form.standaloneServerForm.servicesUnavailable": "Einige der Services werden nicht verfügbar sein oder erfordern eine manuelle Einrichtung", "onboarding.form.standaloneServerForm.publishOwnApp": "Um Push-Benachrichtigungen zu senden, müssen Sie Ihre eigene App kompilieren und in Google Play und im App Store veröffentlichen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 3522dc59e8cb1..001cdf080f7b5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -41,7 +41,7 @@ "A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "A workspace admin needs to install and configure a conference call app.", "An_app_needs_to_be_installed_and_configured": "An app needs to be installed and configured.", "Accessibility": "Accessibility", - "Accessibility_and_Appearance": "Accessibility & Appearance", + "Accessibility_and_Appearance": "Accessibility & appearance", "Accessibility_activation": "Here you can activate a range of features to enhance your browsing experience.", "Accept_Call": "Accept Call", "Accept": "Accept", @@ -415,6 +415,7 @@ "Also_send_to_channel": "Also send to channel", "Always_open_in_new_window": "Always Open in New Window", "Always_show_thread_replies_in_main_channel": "Always show thread replies in main channel", + "Analytic_reports": "Analytic reports", "Analytics": "Analytics", "Analytics_Description": "See how users interact with your workspace.", "Analytics_features_enabled": "Features Enabled", @@ -423,6 +424,8 @@ "Analytics_features_users_Description": "Tracks custom events related to actions related to users (password reset times, profile picture change, etc).", "Analytics_Google": "Google Analytics", "Analytics_Google_id": "Tracking ID", + "Analytics_page_briefing_first_paragraph": "Rocket.Chat collects anonymous usage data, such as feature usage and session lengths, to improve the product for everyone.", + "Analytics_page_briefing_second_paragraph": "We protect your privacy by never collecting personal or sensitive data. This section shows what is collected, reinforcing our commitment to transparency and trust.", "Analyze_practical_usage": "Analyze practical usage statistics about users, messages and channels", "and": "and", "And_more": "And {{length}} more", @@ -528,6 +531,7 @@ "Apps_context_installed": "Installed", "Apps_context_requested": "Requested", "Apps_context_private": "Private Apps", + "Apps_context_premium": "Premium", "Apps_Count_Enabled": "{{count}} app enabled", "Apps_Count_Enabled_plural": "{{count}} apps enabled", "Private_Apps_Count_Enabled": "{{count}} private app enabled", @@ -701,6 +705,8 @@ "Authorization_URL": "Authorization URL", "Authorize": "Authorize", "Authorize_access_to_your_account": "Authorize access to your account", + "Automatic_translation_not_available": "Automatic translation not available", + "Automatic_translation_not_available_info": "This room has E2E encryption enabled, translation cannot work with encrypted messages", "Auto_Load_Images": "Auto Load Images", "Auto_Selection": "Auto Selection", "Auto_Translate": "Auto-Translate", @@ -711,9 +717,14 @@ "AutoTranslate_APIKey": "API Key", "AutoTranslate_Change_Language_Description": "Changing the auto-translate language does not translate previous messages.", "AutoTranslate_DeepL": "DeepL", + "AutoTranslate_Disabled_for_room": "Auto-translate disabled for #{{roomName}}", "AutoTranslate_Enabled": "Enable Auto-Translate", "AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the `auto-translate` permission to have all messages automatically translated into their selected language. Fees may apply.", + "AutoTranslate_Enabled_for_room": "Auto-translate enabled for #{{roomName}}", + "AutoTranslate_AutoEnableOnJoinRoom": "Auto-Translate for non-default language members", + "AutoTranslate_AutoEnableOnJoinRoom_Description": "If enabled, whenever a user with a language preference different than the workspace default joins a room, it will be automatically translated for them.", "AutoTranslate_Google": "Google", + "AutoTranslate_language_set_to": "Auto-translate language set to {{language}}", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", "AutoTranslate_ServiceProvider": "Service Provider", @@ -883,6 +894,7 @@ "Canned_Responses": "Canned Responses", "Canned_Responses_Enable": "Enable Canned Responses", "Create_department": "Create department", + "Create_direct_message": "Create direct message", "Create_tag": "Create tag", "Create_trigger": "Create trigger", "Create_SLA_policy": "Create SLA policy", @@ -1088,6 +1100,7 @@ "Condition": "Condition", "Commit_details": "Commit Details", "Completed": "Completed", + "Compliant_use_of_color": "Compliant use of color", "Computer": "Computer", "Conference_call_apps": "Conference call apps", "Conference_call_has_ended": "_Call has ended._", @@ -1101,8 +1114,14 @@ "Confirm_New_Password_Placeholder": "Please re-enter new password...", "Confirm_password": "Confirm password", "Confirm_your_password": "Confirm your password", + "Confirm_configuration_update_description": "Identification data and cloud connection data will be retained.

Warning: If this is actually a new workspace, please go back and select new workspace option to avoid communication conflicts.", + "Confirm_configuration_update": "Confirm configuration update", + "Confirm_new_workspace_description": "Identification data and cloud connection data will be reset.

Warning: License can be affected if changing workspace URL.", + "Confirm_new_workspace": "Confirm new workspace", "Confirmation": "Confirmation", "Configure_video_conference": "Configure conference call", + "Configuration_update_confirmed": "Configuration update confirmed", + "Configuration_update": "Configuration update", "Connect": "Connect", "Connected": "Connected", "Connect_SSL_TLS": "Connect with SSL/TLS", @@ -1622,7 +1641,7 @@ "Direct": "Direct", "Direction": "Direction", "Livechat_Facebook_API_Secret": "OmniChannel API Secret", - "Direct_Message": "Direct Message", + "Direct_Message": "Direct message", "Livechat_Facebook_Enabled": "Facebook integration enabled", "Direct_message_creation_description": "You are about to create a chat with multiple users. Add the ones you would like to talk, everyone in the same place, using direct messages.", "Direct_message_someone": "Direct message someone", @@ -1662,14 +1681,14 @@ "Discussion": "Discussion", "Discussion_Description": "Discussions are an additional way to organize conversations that allows inviting users from outside channels to participate in specific conversations.", "Discussion_description": "Help keep an overview of what's going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.", - "Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-End encrypted messages in this discussion after its creation.", + "Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-end encrypted messages in this discussion after its creation.", "Discussion_first_message_title": "Your message", "Discussion_name": "Discussion name", "Discussion_start": "Start a Discussion", "Discussion_target_channel": "Parent channel or group", "Discussion_target_channel_description": "Select a channel which is related to what you want to ask", "Discussion_target_channel_prefix": "You are creating a discussion in", - "Discussion_title": "Create a new discussion", + "Discussion_title": "Create discussion", "Discussions_unavailable_for_federation": "Discussions are unavailable for Federated rooms", "discussion-created": "{{message}}", "Discussions": "Discussions", @@ -1725,6 +1744,8 @@ "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", "E2E Encryption": "E2E Encryption", + "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", + "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link", "E2E Encryption_Description": "Keep conversations private, ensuring only the sender and intended recipients are able to read them.", @@ -1737,7 +1758,7 @@ "E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default", "E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default", "E2E_Encryption_Password_Change": "Change Encryption Password", - "E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.

This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.", + "E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.

This is end-to-end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.", "E2E_key_reset_email": "E2E Key Reset Notification", "E2E_message_encrypted_placeholder": "This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.", "E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password.
You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.", @@ -1844,6 +1865,7 @@ "EmojiCustomFilesystem_Description": "Specify how emojis are stored.", "Empty_no_agent_selected": "Empty, no agent selected", "Empty_title": "Empty title", + "Empower_access_move_beyond_color": "Empower access, move beyond color", "Enable": "Enable", "Enable_Auto_Away": "Enable Auto Away", "Enable_CSP": "Enable Content-Security-Policy", @@ -1861,7 +1883,7 @@ "Enable_unlimited_apps": "Enable unlimited apps", "Enabled": "Enabled", "Encrypted": "Encrypted", - "Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", + "Encrypted_channel_Description": "End-to-end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", "Encrypted_key_title": "Click here to disable end-to-end encryption for this channel (requires e2ee-permission)", "Encrypted_message": "Encrypted message", "Encrypted_setting_changed_successfully": "Encrypted setting changed successfully", @@ -2073,6 +2095,7 @@ "error-too-many-requests": "Error, too many requests. Please slow down. You must wait {{seconds}} seconds before trying again.", "error-transcript-already-requested": "Transcript already requested", "error-unpinning-message": "Message could not be unpinned", + "error-user-deactivated": "User is not active", "error-user-has-no-roles": "User has no roles", "error-user-is-not-activated": "User is not activated", "error-user-is-not-agent": "User is not an Omnichannel Agent", @@ -2402,6 +2425,7 @@ "Global_purge_override_warning": "A global retention policy is in place. If you leave \"Override global retention policy\" off, you can only apply a policy that is stricter than the global policy.", "Global_Search": "Global search", "Go_to_your_workspace": "Go to your workspace", + "Go_to_accessibility_and_appearance": "Go to accessibility and appearance", "Google_Meet_Enterprise_only": "Google Meet (Enterprise only)", "Google_Play": "Google Play", "Hold_Call": "Hold Call", @@ -2469,6 +2493,7 @@ "Hospitality_Businness": "Hospitality Business", "hours": "hours", "Hours": "Hours", + "How_and_why_we_collect_usage_data": "How and why usage data is collected", "How_friendly_was_the_chat_agent": "How friendly was the chat agent?", "How_knowledgeable_was_the_chat_agent": "How knowledgeable was the chat agent?", "How_long_to_wait_after_agent_goes_offline": "How Long to Wait After Agent Goes Offline", @@ -2586,6 +2611,7 @@ "Install_FxOs_done": "Great! You can now use Rocket.Chat via the icon on your homescreen. Have fun with Rocket.Chat!", "Install_FxOs_error": "Sorry, that did not work as intended! The following error appeared:", "Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).", + "Installing": "Installing", "Install_package": "Install package", "Installation": "Installation", "Installed": "Installed", @@ -2608,6 +2634,8 @@ "Integration_Incoming_WebHook": "Incoming WebHook Integration", "Integration_New": "New Integration", "integration-scripts-disabled": "Integration Scripts are Disabled", + "integration-scripts-isolated-vm-disabled": "The \"Secure Sandbox\" may not be used on new or modified scripts.", + "integration-scripts-vm2-disabled": "The \"Compatible Sandbox\" may not be used on new or modified scripts.", "Integration_Outgoing_WebHook": "Outgoing WebHook Integration", "Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History", "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration", @@ -2744,6 +2772,10 @@ "Jump_to_message": "Jump to message", "Jump_to_recent_messages": "Jump to recent messages", "Just_invited_people_can_access_this_channel": "Just invited people can access this channel.", + "kick-user-from-any-c-room": "Kick User from Any Public Channel", + "kick-user-from-any-c-room_description": "Permission to kick a user from any public channel", + "kick-user-from-any-p-room": "Kick User from Any Private Channel", + "kick-user-from-any-p-room_description": "Permission to kick a user from any private channel", "Katex_Dollar_Syntax": "Allow Dollar Syntax", "Katex_Dollar_Syntax_Description": "Allow using $$katex block$$ and $inline katex$ syntaxes", "Katex_Enabled": "Katex Enabled", @@ -3318,11 +3350,19 @@ "Members_List": "Members List", "mention-all": "Mention All", "mention-all_description": "Permission to use the @all mention", + "Mentions_all_room_members": "Mentions all room members", + "Mentions_online_room_members": "Mentions online room members", + "Mentions_user": "Mentions user", + "Mentions_channel": "Mentions channel", + "Mentions_you": "Mentions you", "mention-here": "Mention Here", "mention-here_description": "Permission to use the @here mention", "Mentions": "Mentions", "Mentions_default": "Mentions (default)", "Mentions_only": "Mentions only", + "Mentions_with_@_symbol": "Mentions with @ symbol", + "Mentions_with_@_symbol_description": "Mentions notify and highlight messages for groups or specific users, facilitating targeted communication.\n\nThe screen reader functionality is optimized when the \"@\" symbol is employed in the mention feature. This ensures that users relying on screen readers can easily interpret and engage with these mentions.", + "Mentions_with_symbol_upsell_description": "Unlock the full potential of a barrier-free business with our premium accessibility feature.\n\nSay goodbye to color-related compliance challenges all while aligning with WCAG (Web Content Accessibility Guidelines) and BITV (Barrierefreie Informationstechnik-Verordnung) standards.\n\nThe use of the @ symbol makes it easier for screen readers to navigate and interact with your content, ensuring the best experience for all users.", "Merge_Channels": "Merge Channels", "message": "message", "Message": "Message", @@ -3633,6 +3673,8 @@ "New_version_available_(s)": "New version available (%s)", "New_videocall_request": "New Video Call Request", "New_visitor_navigation": "New Navigation: {{history}}", + "New_workspace_confirmed": "New workspace confirmed", + "New_workspace": "New workspace", "Newer_than": "Newer than", "Newer_than_may_not_exceed_Older_than": "\"Newer than\" may not exceed \"Older than\"", "Nickname": "Nickname", @@ -4045,9 +4087,10 @@ "Privacy_summary": "Privacy summary", "Private": "Private", "private": "private", + "Private_channels": "Private channels", "Private_Apps": "Private Apps", "Private_Channel": "Private Channel", - "Private_Channels": "Private Channels", + "Private_Channels": "Private channels", "Private_Chats": "Private Chats", "Private_Group": "Private Group", "Private_Groups": "Private Groups", @@ -4077,7 +4120,7 @@ "Public": "Public", "public": "public", "Public_Channel": "Public Channel", - "Public_Channels": "Public Channels", + "Public_Channels": "Public channels", "Public_Community": "Public Community", "Public_URL": "Public URL", "Purchase_for_free": "Purchase for FREE", @@ -4506,6 +4549,10 @@ "Screen_Share": "Screen Share", "Script": "Script", "Script_Enabled": "Script Enabled", + "Script_Engine": "Script Sandbox", + "Script_Engine_Description": "Older scripts may require the compatible sandbox to run properly, but all new scripts should try to use the secure sandbox instead.", + "Script_Engine_vm2": "Compatible Sandbox (Deprecated)", + "Script_Engine_isolated_vm": "Secure Sandbox", "Search": "Search", "Searchable": "Searchable", "Search_Apps": "Search apps", @@ -4513,6 +4560,7 @@ "Search_Installed_Apps": "Search installed apps", "Search_Private_apps": "Search private apps", "Search_Requested_Apps": "Search requested apps", + "Search_Premium_Apps": "Search Premium apps", "Search_by_file_name": "Search by file name", "Search_by_username": "Search by username", "Search_by_category": "Search by category", @@ -4693,6 +4741,10 @@ "Showing_online_users": "Showing: {{total_showing}}, Online: {{online}}, Total: {{total}} users", "Showing_results": "

Showing %s results

", "Showing_results_of": "Showing results %s - %s of %s", + "Show_usernames": "Show usernames", + "Show_roles": "Show roles", + "Show_or_hide_the_user_roles_of_message_authors": "Show or hide the user roles of message authors.", + "Show_or_hide_the_username_of_message_authors": "Show or hide the username of message authors.", "Sidebar": "Sidebar", "Sidebar_list_mode": "Sidebar Channel List Mode", "Sign_in_to_start_talking": "Sign in to start talking", @@ -4942,7 +4994,7 @@ "Teams_New_Description_Label": "Topic", "Teams_New_Description_Placeholder": "What is this team about", "Teams_New_Encrypted_Description_Disabled": "Only available for private team", - "Teams_New_Encrypted_Description_Enabled": "End to end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.", + "Teams_New_Encrypted_Description_Enabled": "End-to-end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.", "Teams_New_Encrypted_Label": "Encrypted", "Teams_New_Private_Description_Disabled": "When disabled, anyone can join the team", "Teams_New_Private_Description_Enabled": "Only invited people can join", @@ -5141,6 +5193,7 @@ "Transferred": "Transferred", "Translate": "Translate", "Translated": "Translated", + "Translate_to": "Translate to", "Translations": "Translations", "Travel_and_Places": "Travel & Places", "Trigger_removed": "Trigger removed", @@ -5161,10 +5214,8 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "This setting prevents all instances form sending the status changes of the users to their clients keeping all the users with their presence status from the first load!", "Troubleshoot_Disable_Sessions_Monitor": "Disable Sessions Monitor", "Troubleshoot_Disable_Sessions_Monitor_Alert": "This setting stops the processing of user sessions causing the statistics to stop working correctly!", - "Troubleshoot_Disable_Statistics_Generator": "Disable Statistics Generator", - "Troubleshoot_Disable_Statistics_Generator_Alert": "This setting stops the processing all statistics making the info page outdated until someone clicks on the refresh button and may cause other missing information around the system!", - "Troubleshoot_Disable_Workspace_Sync": "Disable Workspace Sync", - "Troubleshoot_Disable_Workspace_Sync_Alert": "This setting stops the sync of this server with Rocket.Chat's cloud and may cause issues with marketplace and enteprise licenses!", + "Troubleshoot_Disable_Teams_Mention": "Disable Teams mention", + "Troubleshoot_Disable_Teams_Mention_Alert": "This setting disables the teams mention feature. User's won't be able to mention a Team by name in a message and get its members notified.", "True": "True", "Try_now": "Try now", "Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead", @@ -5226,6 +5277,9 @@ "Uninstall": "Uninstall", "Units": "Units", "Unit_removed": "Unit Removed", + "Unique_ID_change_detected_description": "Information that identifies this workspace has changed. This can happen when the site URL or database connection string are changed or when a new workspace is created from a copy of an existing database.

Would you like to proceed with a configuration update to the existing workspace or create a new workspace and unique ID?", + "Unique_ID_change_detected_learn_more_link": "Learn more", + "Unique_ID_change_detected": "Unique ID change detected", "Unknown_Import_State": "Unknown Import State", "Unknown_User": "Unknown User", "Unlimited": "Unlimited", @@ -5650,6 +5704,7 @@ "webdav-server-not-found": "WebDAV server not found", "Webhook_Details": "WebHook Details", "Webhook_URL": "Webhook URL", + "Webhook_URL_not_set": "Webhook URL is not set", "Webhooks": "Webhooks", "WebRTC": "WebRTC", "WebRTC_Description": "Broadcast audio and/or video material, as well as transmit arbitrary data between browsers without the need for a middleman.", @@ -5832,7 +5887,7 @@ "onboarding.component.form.action.confirm": "Confirm", "onboarding.component.form.termsAndConditions": "I agree with <1>Terms and Conditions and <3>Privacy Policy", "onboarding.component.emailCodeFallback": "Didn’t receive email? <1>Resend or <3>Change email", - "onboarding.page.form.title": "Let's <1>Launch Your Workspace", + "onboarding.page.form.title": "Let's launch your workspace", "onboarding.page.awaitingConfirmation.title": "Awaiting confirmation", "onboarding.page.awaitingConfirmation.subtitle": "We have sent you an email to {{emailAddress}} with a confirmation link. Please verify that the security code below matches the one in the email.", "onboarding.page.emailConfirmed.title": "Email Confirmed!", @@ -5859,10 +5914,8 @@ "onboarding.page.requestTrial.subtitle": "Try our best Enterprise Edition plan for 30 days for free", "onboarding.page.magicLinkEmail.title": "We emailed you a login link", "onboarding.page.magicLinkEmail.subtitle": "Click the link in the email we just sent you to sign in to your workspace. <1>The link will expire in 30 minutes.", - "onboarding.page.organizationInfoPage.title": "A few more details...", - "onboarding.page.organizationInfoPage.subtitle": "These will help us to personalize your workspace.", "onboarding.form.adminInfoForm.title": "Admin Info", - "onboarding.form.adminInfoForm.subtitle": "We need this to create an admin profile inside your workspace", + "onboarding.form.adminInfoForm.subtitle": "We need this information to create an admin profile for your workspace.", "onboarding.form.adminInfoForm.fields.fullName.label": "Full name", "onboarding.form.adminInfoForm.fields.fullName.placeholder": "First and last name", "onboarding.form.adminInfoForm.fields.username.label": "Username", @@ -5873,7 +5926,7 @@ "onboarding.form.adminInfoForm.fields.password.placeholder": "Create password", "onboarding.form.adminInfoForm.fields.keepPosted.label": "Keep me posted about Rocket.Chat updates", "onboarding.form.organizationInfoForm.title": "Organization Info", - "onboarding.form.organizationInfoForm.subtitle": "Please, bear with us. This info will help us personalize your workspace", + "onboarding.form.organizationInfoForm.subtitle": "We need to know who you are.", "onboarding.form.organizationInfoForm.fields.organizationName.label": "Organization name", "onboarding.form.organizationInfoForm.fields.organizationName.placeholder": "Organization name", "onboarding.form.organizationInfoForm.fields.organizationType.label": "Organization type", @@ -5884,17 +5937,16 @@ "onboarding.form.organizationInfoForm.fields.organizationSize.placeholder": "Select", "onboarding.form.organizationInfoForm.fields.country.label": "Country", "onboarding.form.organizationInfoForm.fields.country.placeholder": "Select", - "onboarding.form.registeredServerForm.title": "Register Your Server", + "onboarding.form.registeredServerForm.title": "Register your workspace", "onboarding.form.registeredServerForm.included.push": "Mobile push notifications", "onboarding.form.registeredServerForm.included.externalProviders": "Integration with external providers (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Access to marketplace apps", - "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud account email", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "To register your server, we need to connect it to your cloud account. If you already have one - we will link it automatically. Otherwise, a new account will be created", - "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Please enter your Email", + "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Admin email", + "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Insert your email to continue", "onboarding.form.registeredServerForm.keepInformed": "Keep me informed about news and events", "onboarding.form.registeredServerForm.registerLater": "Register later", "onboarding.form.registeredServerForm.notConnectedToInternet": "The server is not connected to the internet, so you’ll have to do an offline registration for this workspace.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "By registering I agree to receive relevant product and security updates", + "onboarding.form.registeredServerForm.registrationEngagement": "Registration allows automatic license updates, notifications of critical vulnerabilities and access to Rocket.Chat Cloud services. No sensitive workspace data is shared; statistics sent to Rocket.Chat is made visible to you within the administration area.", "onboarding.form.standaloneServerForm.title": "Standalone Server Confirmation", "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", "onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store", @@ -5905,9 +5957,9 @@ "Theme_light_description": "More accessible for individuals with visual impairments and a good choice for well-lit environments.", "Theme_dark": "Dark", "Theme_dark_description": "Reduce eye strain and fatigue in low-light conditions by minimizing the amount of light emitted by the screen.", - "Enable_of_limit_apps_currently_enabled": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** Disable another {{context}} app or upgrade to Enterprise to enable this app.", - "Enable_of_limit_apps_currently_enabled_exceeded": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nCommunity edition app limit has been exceeded. \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** You will need to disable at least {{exceed}} other {{context}} apps or upgrade to Enterprise to enable this app.", - "Workspaces_on_Community_edition_install_app": "Workspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. Upgrade to Enterprise to enable unlimited apps.", + "Enable_of_limit_apps_currently_enabled": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** Disable another {{context}} app or upgrade to Premium to enable this app.", + "Enable_of_limit_apps_currently_enabled_exceeded": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nCommunity edition app limit has been exceeded. \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** You will need to disable at least {{exceed}} other {{context}} apps or upgrade to Premium to enable this app.", + "Workspaces_on_Community_edition_install_app": "Workspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. Upgrade to Premium to enable unlimited apps.", "Apps_Currently_Enabled": "{{enabled}} of {{limit}} {{context}} apps currently enabled.", "Disable_another_app": "Disable another app or upgrade to Enterprise to enable this app.", "Upload_anyway": "Upload anyway", @@ -5921,16 +5973,17 @@ "Theme_match_system_description": "Automatically match the appearance of your system.", "Theme_high_contrast": "High contrast", "Theme_high_contrast_description": "Maximum tonal differentiation with bold colors and sharp contrasts provide enhanced accessibility.", + "Highlighted_chosen_word": "Highlighted chosen word", "High_contrast_upsell_title": "Enable high contrast theme", "High_contrast_upsell_subtitle": "Enhance your team’s reading experience", - "High_contrast_upsell_description": "Especially designed for individuals with visual impairments or conditions such as color vision deficiency, low vision, or sensitivity to low contrast. \nThis theme increases contrast between text and background elements, making content more distinguishable and easier to read.", + "High_contrast_upsell_description": "Especially designed for individuals with visual impairments or conditions such as color vision deficiency, low vision, or sensitivity to low contrast.\n\nThis theme increases contrast between text and background elements, making content more distinguishable and easier to read.", "High_contrast_upsell_annotation": "Talk to your workspace admin about enabling the high contrast theme for everyone.", "Join_your_team": "Join your team", "Create_a_password": "Create a password", "Create_an_account": "Create an account", "Get_all_apps": "Get all the apps your team needs", - "Workspaces_on_community_edition_trial_on": "Workspaces on Community Edition can have up to 5 marketplace apps and 3 private apps enabled. Start a free Enterprise trial to remove these limits today!", - "Workspaces_on_community_edition_trial_off": "Workspaces on Community Edition can have up to 5 marketplace apps and 3 private apps enabled. Upgrade to Enterprise to remove limits and supercharge your workspace.", + "Workspaces_on_community_edition_trial_on": "Workspaces on Community Edition can have up to 5 marketplace apps and 3 private apps enabled. Start a free Premium trial to remove these limits today!", + "Workspaces_on_community_edition_trial_off": "Workspaces on Community Edition can have up to 5 marketplace apps and 3 private apps enabled. Upgrade to Premium to remove limits and supercharge your workspace.", "No_private_apps_installed": "No private apps installed", "Private_apps_are_side-loaded": "Private apps are side-loaded and are not available on the Marketplace.", "Chat_transcript": "Chat transcript", @@ -6032,5 +6085,15 @@ "All_visible": "All visible", "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", - "Theme_Appearence": "Theme Appearence" + "Theme_Appearence": "Theme Appearence", + "mentions_counter": "{{count}} mention", + "mentions_counter_plural": "{{count}} mentions", + "threads_counter": "{{count}} unread threaded message", + "threads_counter_plural": "{{count}} unread threaded messages", + "group_mentions_counter": "{{count}} group mention", + "group_mentions_counter_plural": "{{count}} group mentions", + "unread_messages_counter": "{{count}} unread message", + "unread_messages_counter_plural": "{{count}} unread messages", + "Premium": "Premium", + "Premium_capability": "Premium capability" } \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 50a7b8d638734..aac9f8bfa4dba 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -4268,10 +4268,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Este ajuste evita que todas las instancias envíen los cambios de estado de los usuarios a sus clientes, lo que mantiene todos los usuarios con su estado de presencia desde la primera carga.", "Troubleshoot_Disable_Sessions_Monitor": "Deshabilitar supervisor de sesiones", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Este ajuste detiene el procesamiento de las sesiones de visita de Omnichannel, lo que provoca que las estadísticas dejen de funcionar correctamente.", - "Troubleshoot_Disable_Statistics_Generator": "Deshabilitar generador de estadísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Este ajuste detiene el procesamiento de todas las estadísticas, lo que provoca que la página de información quede desactualizada hasta que alguien haga clic en el botón para actualizar. Además, puede causar que falte otra información en el sistema.", - "Troubleshoot_Disable_Workspace_Sync": "Deshabilitar sincronización de espacio de trabajo", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede problemas con las licencias de empresas y Marketplace.", "True": "Verdadero", "Try_now": "Intentar ahora", "Try_searching_in_the_marketplace_instead": "Prueba a buscar en Marketplace en su lugar", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "Prueba nuestro mejor plan Enterprise Edition gratis durante 30 días", "onboarding.page.magicLinkEmail.title": "Te hemos enviado un enlace de inicio de sesión por correo electrónico", "onboarding.page.magicLinkEmail.subtitle": "Haz clic en el enlace del mensaje que acabamos de enviarte para iniciar sesión en tu espacio de trabajo. <1>El enlace caducará en 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Unos detalles más...", - "onboarding.page.organizationInfoPage.subtitle": "Esto nos ayudará a personalizar tu espacio de trabajo.", "onboarding.form.adminInfoForm.title": "Información de administrador", "onboarding.form.adminInfoForm.subtitle": "Necesitamos esto para crear un perfil de administrador en tu espacio de trabajo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nombre completo", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con proveedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso al Marketplace de aplicaciones", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cuenta de correo electrónico en la nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar el servidor, necesitamos conectarlo a tu cuenta en la nube. Si ya tienes una, la vincularemos automáticamente. De lo contrario, se creará una cuenta nueva", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce tu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Recibir información sobre noticias y eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Al registrarme, acepto recibir actualizaciones sobre seguridad y productos relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación de servidor independiente", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algunos servicios no estarán disponibles o requerirán configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviarte notificaciones push, debes compilar y publicar tu propia aplicación en Google Play y App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index ccdec04a7beca..79a27d83cbe61 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -4917,10 +4917,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Tämä asetus estää kaikkia instansseja lähettämästä käyttäjien tilamuutoksia asiakkailleen, jolloin kaikki käyttäjät pysyvät läsnäolotilassaan ensimmäisestä latauksesta lähtien!", "Troubleshoot_Disable_Sessions_Monitor": "Istuntojen valvonnan poistaminen käytöstä", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Tämä asetus pysäyttää käyttäjäistuntojen käsittelyn, jolloin tilastot eivät enää toimi oikein!", - "Troubleshoot_Disable_Statistics_Generator": "Poista tilastogeneraattori käytöstä", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Tämä asetus pysäyttää kaikkien tilastojen käsittelyn, jolloin infosivu on vanhentunut, kunnes joku klikkaa päivityspainiketta, ja se voi aiheuttaa muita puuttuvia tietoja järjestelmästä!", - "Troubleshoot_Disable_Workspace_Sync": "Työtilan synkronoinnin poistaminen käytöstä", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Tämä asetus estää tämän palvelimen synkronoinnin Rocket.Chatin pilven kanssa ja saattaa aiheuttaa ongelmia markkinapaikan ja enteprise-lisenssien kanssa!", "True": "Tosi", "Try_now": "Kokeile nyt", "Try_searching_in_the_marketplace_instead": "Kokeile sen sijaan etsiä Kauppapaikalta", @@ -5603,8 +5599,6 @@ "onboarding.page.requestTrial.subtitle": "Kokeile parasta yritysversion sopimustamme 30 päivää maksutta", "onboarding.page.magicLinkEmail.title": "Lähetimme sinulle kirjautumislinkin sähköpostitse", "onboarding.page.magicLinkEmail.subtitle": "Klikkaa juuri lähettämässämme sähköpostiviestissä olevaa linkkiä kirjautuaksesi työtilaasi. <1>Linkki päättyy 30 minuutin kuluttua.", - "onboarding.page.organizationInfoPage.title": "Muutama yksityiskohta vielä...", - "onboarding.page.organizationInfoPage.subtitle": "Nämä auttavat meitä muokkaamaan työtilasi yksilölliseksi.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Tarvitsemme tätä luodaksemme ylläpitäjäprofiilin työtilaasi", "onboarding.form.adminInfoForm.fields.fullName.label": "Koko nimi", @@ -5633,12 +5627,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrointi ulkoisten palveluntarjoajien kanssa (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Pääsy kauppapaikan sovelluksiin", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-tilin sähköposti", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Rekisteröidäksemme palvelimesi meidän on yhdistettävä se pilvitiliisi. Jos sinulla on jo sellainen - yhdistämme sen automaattisesti. Muussa tapauksessa luodaan uusi tili", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Kirjoita sähköpostiosoitteesi", "onboarding.form.registeredServerForm.keepInformed": "Pidä minut ajan tasalla uutisista ja tapahtumista", "onboarding.form.registeredServerForm.registerLater": "Rekisteröidy myöhemmin", "onboarding.form.registeredServerForm.notConnectedToInternet": "Palvelin ei ole yhteydessä internetiin, joten työtila on rekisteröitävä offline-tilassa.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rekisteröitymällä suostun vastaanottamaan asiaankuuluvia tuote- ja tietoturvapäivityksiä", "onboarding.form.standaloneServerForm.title": "Itsenäisen palvelimen vahvistus", "onboarding.form.standaloneServerForm.servicesUnavailable": "Jotkin palvelut eivät ole käytettävissä tai vaativat manuaalista asennusta", "onboarding.form.standaloneServerForm.publishOwnApp": "Jotta voit lähettää push-ilmoituksia, sinun on koottava ja julkaistava oma sovelluksesi Google Play- ja App Store -sovelluksissa", @@ -5757,4 +5749,4 @@ "Uninstall_grandfathered_app": "Poistetaanko {{appName}}?", "App_will_lose_grandfathered_status": "**Tämä {{context}}sovellus menettää aikaisemmin käytetössä olleen sovelluksen tilansa.** \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}} sovellusta. aikaisemmin Aikaisemmin käytössä olleet sovellukset lasketaan mukaan rajoitukseen, mutta rajoitusta ei sovelleta niihin.", "Theme_Appearence": "Teeman ulkoasu" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 08641b831f940..f460fc0b61dea 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -4300,10 +4300,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ce paramètre empêche toutes les instances d'envoyer les changements de statut des utilisateurs à leurs clients ; le statut de présence du premier chargement est donc conservé !", "Troubleshoot_Disable_Sessions_Monitor": "Désactiver le moniteur de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ce paramètre arrête le traitement des sessions utilisateur, ce qui empêche les statistiques de fonctionner correctement !", - "Troubleshoot_Disable_Statistics_Generator": "Désactiver le générateur de statistiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ce paramètre arrête le traitement de toutes les statistiques, ce qui rend la page d'informations obsolète jusqu'à ce que quelqu'un clique sur le bouton d'actualisation ; d'autres informations peuvent être manquantes dans le système !", - "Troubleshoot_Disable_Workspace_Sync": "Désactiver la synchronisation de l'espace de travail", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ce paramètre arrête la synchronisation de ce serveur avec le cloud Rocket.Chat et peut entraîner des problèmes avec les licences marketplace et entreprise !", "True": "Vrai", "Try_now": "Essayer maintenant", "Try_searching_in_the_marketplace_instead": "Essayez plutôt de chercher sur le marketplace", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "Essayez notre meilleur forfait Enterprise Edition gratuitement pendant 30 jours", "onboarding.page.magicLinkEmail.title": "Nous vous avons envoyé un lien de connexion par e-mail", "onboarding.page.magicLinkEmail.subtitle": "Cliquez sur le lien dans l'e-mail que nous venons de vous envoyer pour vous connecter à votre espace de travail. <1>Le lien expirera dans 30 minutes.", - "onboarding.page.organizationInfoPage.title": "Quelques détails supplémentaires...", - "onboarding.page.organizationInfoPage.subtitle": "Ceux-ci nous aideront à personnaliser votre espace de travail.", "onboarding.form.adminInfoForm.title": "Infos sur l'administrateur", "onboarding.form.adminInfoForm.subtitle": "Nous en avons besoin pour créer un profil d'administrateur dans votre espace de travail", "onboarding.form.adminInfoForm.fields.fullName.label": "Nom complet", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Intégration avec des fournisseurs externes (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Accès aux applications du marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail du compte cloud", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Pour enregistrer votre serveur, nous devons le connecter à votre compte cloud. Si vous en avez déjà un, nous l'associerons automatiquement. Sinon, un nouveau compte sera créé", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Entrez votre adresse e-mail", "onboarding.form.registeredServerForm.keepInformed": "Me tenir informé des actualités et des événements", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "En m'inscrivant, j'accepte de recevoir des mises à jour pertinentes concernant les produits et la sécurité", "onboarding.form.standaloneServerForm.title": "Confirmation du serveur autonome", "onboarding.form.standaloneServerForm.servicesUnavailable": "Certains services ne seront pas disponibles ou nécessiteront une configuration manuelle", "onboarding.form.standaloneServerForm.publishOwnApp": "Pour envoyer des notifications push, vous devez compiler et publier votre propre application sur Google Play et App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json index 5272cd9e1d3f8..9ee622195a6ac 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json @@ -352,8 +352,6 @@ "onboarding.page.requestTrial.subtitle": "Proba o noso mellor plana de empresas durante 30 días de balde", "onboarding.page.magicLinkEmail.title": "Enviámosche por correo electrónico un link de inicio de sesión", "onboarding.page.magicLinkEmail.subtitle": "Fai clic na ligazón do correo electrónico que che acabamos de enviar para iniciar sesión no teu espazo de traballo. <1>A ligazón caducará en 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Algúns detalles máis...", - "onboarding.page.organizationInfoPage.subtitle": "Estes axudaranos a personalizar o teu espazo de traballo.", "onboarding.form.adminInfoForm.title": "Información administrativa", "onboarding.form.adminInfoForm.subtitle": "Necesitamos isto para crear un perfil de administrador dentro do teu espazo de traballo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -382,10 +380,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso a aplicacións do mercado", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Correo electrónico da conta na nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para rexistrar o teu servidor, necesitamos conectalo á túa conta na nube. Se xa tes un, vincularémolo automaticamente. En caso contrario, crearase unha nova conta", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce o teu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Mantéñame informado sobre novidades e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao rexistrarme, acepto recibir actualizacións de produtos e seguridade relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación do servidor autónomo", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algúns dos servizos non estarán dispoñibles ou requirirán unha configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificacións push, debes compilar e publicar a túa propia aplicación en Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index d939e8ad16bb5..1177a356dc71c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -4736,10 +4736,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ez a beállítás megakadályozza az összes példányt abban, hogy elküldjék a felhasználók állapotváltozásait az ügyfeleiknek, megtartva az összes felhasználót az első betöltésből származó jelenléti állapotával!", "Troubleshoot_Disable_Sessions_Monitor": "Munkamenetek megfigyelőjének letiltása", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ez a beállítás leállítja a felhasználók munkameneteinek feldolgozását, ami a statisztikák megfelelő működésének megszűnését okozza!", - "Troubleshoot_Disable_Statistics_Generator": "Statisztika-előállító letiltása", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ez a beállítás leállítja az összes statisztika feldolgozását, ami az információs oldalt elavulttá teszi, amíg valaki nem kattint a frissítés gombra, valamint más információk hiányát is okozhatja a rendszerben!", - "Troubleshoot_Disable_Workspace_Sync": "Munkaterület szinkronizálásának letiltása", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ez a beállítás leállítja ennek a kiszolgálónak a Rocket.Chat felhőjével való szinkronizálását, és problémákat okozhat a piactérrel és a vállalati licencekkel!", "True": "Igaz", "Try_now": "Próbálja most", "Try_searching_in_the_marketplace_instead": "Próbáljon inkább a piactéren keresni", @@ -5393,8 +5389,6 @@ "onboarding.page.requestTrial.subtitle": "Próbálja ki a legjobb vállalati kiadású előfizetéses csomagunkat 30 napig ingyen", "onboarding.page.magicLinkEmail.title": "Elküldünk Önnek egy bejelentkezési hivatkozást e-mailben", "onboarding.page.magicLinkEmail.subtitle": "Kattintson a most elküldött levélben lévő hivatkozásra, hogy bejelentkezhessen a munkaterületére. <1>A hivatkozás 30 percen belül lejár.", - "onboarding.page.organizationInfoPage.title": "Néhány további részlet…", - "onboarding.page.organizationInfoPage.subtitle": "Ezek segítenek nekünk személyre szabni a munkaterületét.", "onboarding.form.adminInfoForm.title": "Adminisztrátor-információk", "onboarding.form.adminInfoForm.subtitle": "Erre azért van szükségünk, hogy létrehozzunk egy adminisztrátori profilt a munkaterületén belül", "onboarding.form.adminInfoForm.fields.fullName.label": "Teljes név", @@ -5423,10 +5417,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integráció külső szolgáltatókkal (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Hozzáférés a piactér alkalmazásaihoz", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Felhős fiók e-mail-címe", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "A kiszolgálója regisztrálásához csatlakoztatnunk kell azt a felhős fiókjához. Ha már rendelkezik ilyennel, akkor automatikusan összekapcsoljuk. Ellenkező esetben új fiók kerül létrehozásra.", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Adja meg az e-mail-címét", "onboarding.form.registeredServerForm.keepInformed": "Tájékoztassanak a hírekről és az eseményekről", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "A regisztrációval beleegyezem, hogy megkapom a vonatkozó termék- és biztonsági frissítéseket", "onboarding.form.standaloneServerForm.title": "Egyedülálló kiszolgáló megerősítése", "onboarding.form.standaloneServerForm.servicesUnavailable": "Néhány szolgáltatás nem lesz elérhető, vagy kézi beállítást igényel", "onboarding.form.standaloneServerForm.publishOwnApp": "A leküldéses értesítések küldéséhez saját alkalmazást kell összeállítania és közzétennie a Google Play és az App Store áruházakban", @@ -5438,4 +5430,4 @@ "Join_your_team": "Csatlakozás csapathoz", "Create_an_account": "Fiók létrehozása", "RegisterWorkspace_Features_Marketplace_Title": "Piactér" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 0bf0eb5063aea..31973b5da92a6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -4259,10 +4259,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "この設定は、すべてのインスタンスがユーザーのステータス変更をクライアントに送信することを防ぎ、すべてのユーザーを最初のロードからのプレゼンスステータスで維持します!", "Troubleshoot_Disable_Sessions_Monitor": "セッションモニターを無効にする", "Troubleshoot_Disable_Sessions_Monitor_Alert": "この設定により、ユーザーセッションの処理が停止し、統計が正しく機能しなくなります!", - "Troubleshoot_Disable_Statistics_Generator": "統計ジェネレーターを無効にする", - "Troubleshoot_Disable_Statistics_Generator_Alert": "この設定では、更新ボタンがクリックされるまですべての統計処理が停止され、情報ページの情報が最新ではなくなり、システムに関するその他の情報が失われる可能性があります。", - "Troubleshoot_Disable_Workspace_Sync": "ワークスペース同期を無効にする", - "Troubleshoot_Disable_Workspace_Sync_Alert": "この設定により、このサーバーとRocket.Chatのクラウドとの同期が停止し、マーケットプレイスとエンタープライズライセンスで問題が発生する可能性があります!", "True": "はい", "Try_now": "今すぐ再試行", "Try_searching_in_the_marketplace_instead": "代わりにマーケットプレイスを検索してみてください", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "30日間の最上位のEnterprise Editionプランを無料でお試しください", "onboarding.page.magicLinkEmail.title": "ログインリンクをメールで送信しました", "onboarding.page.magicLinkEmail.subtitle": "送信されたメールのリンクをクリックし、ワークスペースにサインインしてください。 <1>リンクの有効期間は30分です。", - "onboarding.page.organizationInfoPage.title": "その他の詳細...", - "onboarding.page.organizationInfoPage.subtitle": "これにより、ワークスペースをパーソナライズできます。", "onboarding.form.adminInfoForm.title": "管理者情報", "onboarding.form.adminInfoForm.subtitle": "これはワークスペース内に管理プロファイルを作成するために必要です", "onboarding.form.adminInfoForm.fields.fullName.label": "氏名", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "外部プロバイダー(WhatsApp、Facebook、Telegram、Twitter)との統合", "onboarding.form.registeredServerForm.included.apps": "マーケットプレイスアプリにアクセス", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "クラウドアカウントメール", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "サーバーを登録するには、サーバーをクラウドアカウントに接続する必要があります。アカウントをすでにお持ちの場合は、自動的にリンクします。それ以外の場合は、新しいアカウントが作成されます", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "メールアドレスを入力してください", "onboarding.form.registeredServerForm.keepInformed": "ニュースとイベントの情報を受け取る", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "登録すると、関連する製品とセキュリティの更新を受け取ることに同意したものとみなされます", "onboarding.form.standaloneServerForm.title": "スタンドアロンサーバーの確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "一部のサービスは利用できないか、手動で設定する必要があります", "onboarding.form.standaloneServerForm.publishOwnApp": "プッシュ通知を送信するには、独自のアプリをコンパイルしてGoogle PlayとApp Storeに公開する必要があります", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 15514a726e5be..6dd72917fe9d9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -3303,10 +3303,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "ეს პარამეტრი ყვენა ინსტანციისთვის თიშავს მომხმარებლის სტატუსის გადაგზავნას კლიენტებისთვის და ტოვებს ყველა მომხმარებელს იმ სტატუსით რომლითაც იყო პირველი ჩატვირთვისას.", "Troubleshoot_Disable_Sessions_Monitor": "გამორთეთ სესიების მონიტორი", "Troubleshoot_Disable_Sessions_Monitor_Alert": "ეს პარამეტრი თიშავს მომხმარებლის სესიების დამუშავებას და იწვევს სტატისტიკის არასწორ მუშაობას", - "Troubleshoot_Disable_Statistics_Generator": "გამორთეთ სტატისტიკის გენერატორი", - "Troubleshoot_Disable_Statistics_Generator_Alert": "ეს პარამეტრი სტატისტიკის დამუშავებას თიშავს სრულად და გვერდი ხდება ვადაგასული ვიდრე ვინმე განახლების ღილაკს არ დააჭერს, ამან შეიძლება გამოიწვიოს ზოგი ინფორმაციის დაკარგვა", - "Troubleshoot_Disable_Workspace_Sync": "გამორთეთ სამუშაო ადგილის სინქრონიზაცია", - "Troubleshoot_Disable_Workspace_Sync_Alert": "ეს პარამეტრი თიშავს ამ სერვერის Rocket.Chat's clou-თან სინქრონიზაციას და შეიძლება გამოიწვიოს პრობლემები მარკეტში და საწარმო ლიცენზიებში!", "True": "მართალია", "Try_now": "სცადე ახლა", "Tuesday": "სამშაბათი", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 34f7608c9bed2..2574c0b288b8a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -3624,10 +3624,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "이 설정은 모든 인스턴스가 사용자의 상태 변경 사항을 클라이언트로 보내지 않으며, 설정 시, 모든 사용자의 상태가 처음 로딩상태로 유지됩니다.!", "Troubleshoot_Disable_Sessions_Monitor": "세션 모니터링 사용중지", "Troubleshoot_Disable_Sessions_Monitor_Alert": "이 설정은 사용자 세션 처리를 중단하는 것입니다. 설정 시 통계가 올바르게 작동하지 않을 수 있습니다. ", - "Troubleshoot_Disable_Statistics_Generator": "통계 생성 사용중지", - "Troubleshoot_Disable_Statistics_Generator_Alert": "이 설정은 누군가가 새로 고침 버튼을 클릭 할 때까지 이전 정보 페이지를 생성하는 모든 통계 처리를 중지하는 것입니다. 설정 시 시스템 주변에 다른 정보가 누락 될 수 있습니다!", - "Troubleshoot_Disable_Workspace_Sync": " Workspace 동기화 사용중지", - "Troubleshoot_Disable_Workspace_Sync_Alert": "이 설정은 서버와 Rocket.Chat의 클라우드 동기화를 중지하는 것입니다. 설정 시 Marketplace 및 기업 라이선스에 문제가 발생할 수 있습니다. ", "True": "설정됨", "Try_now": "지금 시도", "Tuesday": "화요일", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index f611b39aebd48..eff6811050c1c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -4290,10 +4290,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Deze instelling voorkomt dat alle instanties de statuswijzingen van de gebruikers naar hun clients sturen, waarbij alle gebruikers hun aanwezigheidsstatus behouden van de eerste lading!", "Troubleshoot_Disable_Sessions_Monitor": "Schakel sessies monitor uit", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Deze instelling stopt de verwerking van gebruikerssessies waardoor de statistieken niet meer correct werken!", - "Troubleshoot_Disable_Statistics_Generator": "Schakel statistieken generator uit", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Deze instelling stopt de verwerking van alle statistieken waardoor de info-pagina verouderd raakt totdat iemand op de Vernieuwen knop klikt, en kan ontbrekende informatie in het systeem verzoorzaken!", - "Troubleshoot_Disable_Workspace_Sync": "Schakel Workspace Sync uit", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Deze instelling stopt de synchronisatie van deze server met Rocket.Chat's cloud en kan problemen veroorzaken met marktplaats- en bedrijfslicenties!", "True": "Waar", "Try_now": "Probeer nu", "Try_searching_in_the_marketplace_instead": "Probeer in plaats daarvan in de Marketplace te zoeken", @@ -4843,8 +4839,6 @@ "onboarding.page.requestTrial.subtitle": "Probeer gratis onze beste Enterprise Edition-abonnement gedurende 30 dagen", "onboarding.page.magicLinkEmail.title": "We hebben je een inloglink gemaild.", "onboarding.page.magicLinkEmail.subtitle": "Klik op de link in de e-mail die we u zojuist hebben gestuurd om u aan te melden bij uw werkruimte. <1>De link verloopt over 30 minuten.", - "onboarding.page.organizationInfoPage.title": "Nog een paar details...", - "onboarding.page.organizationInfoPage.subtitle": "Deze zullen ons helpen om uw werkruimte te personaliseren.", "onboarding.form.adminInfoForm.title": "Admin info", "onboarding.form.adminInfoForm.subtitle": "We hebben dit nodig om een beheerdersprofiel in uw werkruimte te maken", "onboarding.form.adminInfoForm.fields.fullName.label": "Volledige naam", @@ -4873,10 +4867,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integratie met externe providers (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Toegang tot Marketplace-apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mailadres van cloudaccount", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Om uw server te registreren, moeten we deze verbinden met uw cloudaccount. Als u er al een heeft, zullen we deze automatisch koppelen. Anders wordt er een nieuwe account aangemaakt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Voer uw e-mailadres in", "onboarding.form.registeredServerForm.keepInformed": "Hou me op de hoogte van nieuws en evenementen", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Door te registreren ga ik akkoord met het ontvangen van relevante product- en beveiligingsupdates", "onboarding.form.standaloneServerForm.title": "Standalone serverbevestiging", "onboarding.form.standaloneServerForm.servicesUnavailable": "Sommige diensten zullen niet beschikbaar zijn of vereisen handmatige configuratie", "onboarding.form.standaloneServerForm.publishOwnApp": "Om pushmeldingen te verzenden, moet u uw eigen app compileren en publiceren in Google Play en App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 0c4b85e588e05..80ab48d383c00 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -4667,10 +4667,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "To ustawienie zapobiega wysyłaniu przez wszystkie instancje zmian statusu użytkowników do ich klientów, utrzymując status wszystkich użytkowników taki jak przy pierwszym załadowaniu!", "Troubleshoot_Disable_Sessions_Monitor": "Wyłącz monitor sesji", "Troubleshoot_Disable_Sessions_Monitor_Alert": "To ustawienie zatrzymuje przetwarzanie sesji użytkowników, co spowoduje niepoprawne działanie statystyk!", - "Troubleshoot_Disable_Statistics_Generator": "Wyłącz generator statystyk", - "Troubleshoot_Disable_Statistics_Generator_Alert": "To ustawienie zatrzymuje przetwarzanie wszystkich statystyk powodując, że strona informacyjna stanie się nieaktualna dopóki nie zostanie naciśnięty przycisk odświeżania i może wywołać utratę innych informacji w całym systemie!", - "Troubleshoot_Disable_Workspace_Sync": "Wyłączenie Workspace Sync", - "Troubleshoot_Disable_Workspace_Sync_Alert": "To ustawienie zatrzymuje synchronizację tego serwera z chmurą Rocket.Chat co może wywołać problemy z marketplace i licencjami korporacyjnymi!", "True": "Tak", "Try_now": "Spróbuj teraz", "Try_searching_in_the_marketplace_instead": "Zamiast tego spróbuj poszukać w Marketplace", @@ -5304,8 +5300,6 @@ "onboarding.page.requestTrial.subtitle": "Wypróbuj nasz najlepszy plan Enterprise Edition przez 30 dni za darmo", "onboarding.page.magicLinkEmail.title": "Wysłaliśmy Ci link do logowania", "onboarding.page.magicLinkEmail.subtitle": "Kliknij link w wiadomości e-mail, którą właśnie do Ciebie wysłaliśmy, aby zalogować się do swojego obszaru roboczego. <1>Link wygaśnie za 30 minut.", - "onboarding.page.organizationInfoPage.title": "Jeszcze kilka szczegółów...", - "onboarding.page.organizationInfoPage.subtitle": "Pomogą nam one spersonalizować Twoje miejsce pracy.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Potrzebujemy tego, aby utworzyć profil administratora w twoim obszarze roboczym", "onboarding.form.adminInfoForm.fields.fullName.label": "Pełna nazwa", @@ -5334,10 +5328,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integracja z zewnętrznymi dostawcami (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Dostęp do aplikacji w Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail konta w chmurze", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Aby zarejestrować Twój serwer, musimy połączyć go z Twoim kontem w chmurze. Jeśli już je posiadasz - połączymy je automatycznie. W przeciwnym razie, zostanie utworzone nowe konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Proszę wpisać swój adres e-mail", "onboarding.form.registeredServerForm.keepInformed": "Informuj mnie o nowościach i wydarzeniach", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rejestrując się, wyrażam zgodę na otrzymywanie odpowiednich aktualizacji produktów i zabezpieczeń", "onboarding.form.standaloneServerForm.title": "Potwierdzenie serwera standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usług będą niedostępne lub będą wymagały ręcznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyłania powiadomień push należy skompilować i opublikować własną aplikację w Google Play i App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index de5c2356a6b9c..7dd1b47f335e7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -621,6 +621,8 @@ "Author_Site": "Página do autor", "Authorization_URL": "URL de autorização", "Authorize": "Autorizar", + "Automatic_translation_not_available": "Tradução automática indisponível", + "Automatic_translation_not_available_info": "Esta sala tem criptografia E2E ativada, a tradução não pode funcionar com mensagens criptografadas", "Auto_Load_Images": "Carregar imagens automaticamente", "Auto_Selection": "Seleção automática", "Auto_Translate": "Traduzir automaticamente", @@ -631,9 +633,12 @@ "AutoTranslate_APIKey": "Key da API", "AutoTranslate_Change_Language_Description": "Alterar o idioma de tradução automática não traduz mensagens anteriores.", "AutoTranslate_DeepL": "DeepL", + "AutoTranslate_Disabled_for_room": "Tradução automática desabilitada para #{{roomName}}", "AutoTranslate_Enabled": "Habilitar tradução automática", "AutoTranslate_Enabled_Description": "Habilitar a tradução automática implicará em permitir que as pessoas com a permissão `auto-translate` tenham todas as suas mensagens automaticamente traduzidas para seu idioma. Taxas podem ser cobradas.", + "AutoTranslate_Enabled_for_room": "Tradução automática habilitada para #{{roomName}}", "AutoTranslate_Google": "Google", + "AutoTranslate_language_set_to": "Linguagem para tradução automática definida como {{language}}", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", "AutoTranslate_ServiceProvider": "Provedor de serviço", @@ -1515,6 +1520,7 @@ "Markdown_Marked_Tables": "Ativar tabelas marcadas", "duplicated-account": "Conta duplicada", "E2E Encryption": "Criptografia E2E", + "E2E_Encryption_enabled_for_room": "Criptografia E2E habilitada para #{{roomName}}", "Markdown_Parser": "Parser de marcação", "Markdown_SupportSchemesForLink": "Esquemas de links compatíveis com marcação", "Markdown_SupportSchemesForLink_Description": "Lista de esquemas permitidos separados por vírgulas", @@ -4336,6 +4342,7 @@ "Transferred": "Transferido", "Translate": "Traduzir", "Translated": "Traduzido", + "Translate_to": "Traduzir para", "Translations": "Traduções", "Travel_and_Places": "Viagem e Locais", "Trigger_removed": "Gatilho removido", @@ -4355,10 +4362,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Esta configuração impede todas as instâncias de enviar as alterações de status dos usuários aos seus clientes, mantendo o status de presença do primeiro carregamento!", "Troubleshoot_Disable_Sessions_Monitor": "Desativar monitor de sessões", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Esta configuração interrompe o processamento das sessões do usuário, fazendo com que as estatísticas parem de funcionar corretamente!", - "Troubleshoot_Disable_Statistics_Generator": "Desativar gerador de estatísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Esta configuração interrompe o processamento de todas as estatísticas, tornando a informação da página desatualizada até que alguém clique no botão Atualizar, e poderá causar perda de outras informações em todo o sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desativa a sincronização do espaço de trabalho", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Esta configuração interrompe a sincronização deste servidor com a nuvem do Rocket.Chat e pode causar problemas com licenças de marketplace e enteprise!", "True": "Verdadeiro", "Try_now": "Tentar agora", "Try_searching_in_the_marketplace_instead": "Tente pesquisar no marketplace", @@ -4705,14 +4708,14 @@ "VoIP_Server_Websocket_Port": "Porta do webSocket", "VoIP_Server_Name": "Nome do servidor", "VoIP_Server_Websocket_Path": "URL do webSocket", + "VoIP_Retry_Count": "Número de tentativas", + "VoIP_Retry_Count_Description": "Define quantas vezes o cliente tentará se reconectar ao servidor VoIP caso a conexão seja perdida.", "VoIP_Management_Server": "Servidor de gerenciamento de VoIP", "VoIP_Management_Server_Host": "Host de servidor", "VoIP_Management_Server_Port": "Porta do servidor", "VoIP_Management_Server_Name": "Nome do servidor", "VoIP_Management_Server_Username": "Nome de usuário", "VoIP_Management_Server_Password": "Senha", - "VoIP_Retry_Count": "Número de tentativas", - "VoIP_Retry_Count_Description": "Define quantas vezes o cliente tentará se reconectar ao servidor VoIP caso a conexão seja perdida.", "Voip_call_started": "Chamada iniciada às", "Voip_call_duration": "Chamada com duração de {{duration}}", "Voip_call_declined": "Chamada finalizada pelo agente", @@ -4752,6 +4755,7 @@ "webdav-account-updated": "Conta WebDAV atualizada", "Webhook_Details": "Detalhes do WebHook", "Webhook_URL": "URL do webhook", + "Webhook_URL_not_set": "A URL do webhook não foi definida", "Webhooks": "Webhooks", "WebRTC_Call": "Chamada WebRTC", "WebRTC_direct_audio_call_from_%s": "Chamada de áudio direta de %s", @@ -4921,8 +4925,6 @@ "onboarding.page.requestTrial.subtitle": "Experimento nosso melhor plano Enterprise Edition grátis por 30 dias", "onboarding.page.magicLinkEmail.title": "Nós enviamos um link de login por e-mail", "onboarding.page.magicLinkEmail.subtitle": "Clique no link no e-mail que enviamos para iniciar sessão em seu espaço de trabalho. <1>O link vai expirar em 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Mais alguns detalhes...", - "onboarding.page.organizationInfoPage.subtitle": "Isso nos ajudará a personalizar seu espaço de trabalho.", "onboarding.form.adminInfoForm.title": "Informação administrativa", "onboarding.form.adminInfoForm.subtitle": "Precisamos disso para criar um perfil de administração dentro do seu espaço de trabalho.", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -4951,10 +4953,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integração com provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acesso a aplicativos de Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail da conta da nuvem", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar seu servidor, precisamos conectar à sua conta da nuvem. Se você já tem uma - nós conectaremos a ela automaticamente. Caso contrário, uma nova conta será criada", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Insira seu e-mail", "onboarding.form.registeredServerForm.keepInformed": "Mantenha-me informado sobre notícias e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao registrar, eu concordo em receber atualizações relevantes do produto e de segurança", "onboarding.form.standaloneServerForm.title": "Confirmação de servidor standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponíveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 8f71b6f49ab22..48557a3a146ed 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -4463,10 +4463,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Эта настройка не позволяет всем инстансам отправлять изменения статуса пользователей своим клиентам, сохраняя всех пользователей со статусом присутствия с первой загрузки!", "Troubleshoot_Disable_Sessions_Monitor": "Отключить монитор сессий", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Эта настройка останавливает обработку пользовательских сессий, в результате чего статистика перестает работать корректно!", - "Troubleshoot_Disable_Statistics_Generator": "Отключить генератор статистики", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Эта настройка останавливает обработку всей статистики, делая информационную страницу устаревшей до тех пор, пока кто-нибудь не нажмет кнопку обновления, это может привести к появлению другой недостающей информации в системе!", - "Troubleshoot_Disable_Workspace_Sync": "Отключить синхронизацию рабочего пространства", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Эта настройка останавливает синхронизацию данного сервера с Rocket.Chat Cloud и может привести к проблемам с корпоративами лицензиями и в магазине приложений!", "True": "Да", "Try_now": "Попробуйте сейчас", "Try_searching_in_the_marketplace_instead": "Попробуйте выполнить поиск в магазине", @@ -5041,8 +5037,6 @@ "onboarding.page.requestTrial.subtitle": "Воспользуйтесь нашим лучшим тарифным планом Enterprise Edition в течение 30 дней бесплатно", "onboarding.page.magicLinkEmail.title": "Мы отправили вам ссылку для входа в систему в электронном письме", "onboarding.page.magicLinkEmail.subtitle": "Нажмите на ссылку в электронном письме, чтобы войти в свое рабочее пространство. <1>Срок действия ссылки истечет через 30 минут.", - "onboarding.page.organizationInfoPage.title": "Дополнительные сведения...", - "onboarding.page.organizationInfoPage.subtitle": "Это поможет нам персонализировать ваше рабочее пространство.", "onboarding.form.adminInfoForm.title": "Информация об администраторе", "onboarding.form.adminInfoForm.subtitle": "Это необходимо для создания профиля администратора в вашем рабочем пространстве", "onboarding.form.adminInfoForm.fields.fullName.label": "Полное имя", @@ -5071,10 +5065,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Интеграция с внешними поставщиками (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Доступ к приложениям магазина", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Адрес электронной почты учетной записи в облаке", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Чтобы зарегистрировать сервер, необходимо подключить его к учетной записи облака. Если у вас уже есть такая учетная запись, мы свяжем ее автоматически. В противном случае будет создана новая учетная запись", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Введите адрес электронной почты", "onboarding.form.registeredServerForm.keepInformed": "Сообщайте мне новости и информацию о событиях", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Регистрируясь, я соглашаюсь получать соответствующие обновления продуктов и системы безопасности", "onboarding.form.standaloneServerForm.title": "Подтверждение автономного сервера", "onboarding.form.standaloneServerForm.servicesUnavailable": "Некоторые сервисы будут недоступны или потребуется ручная настройка", "onboarding.form.standaloneServerForm.publishOwnApp": "Чтобы отправлять push-уведомления, необходимо создать и опубликовать собственное приложение в Google Play и App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 35792cce086a6..5cf724dd49c1b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -4922,10 +4922,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Med den här inställningen förhindras alla instanser att skicka ändringar av användarnas status till deras klienter, vilket gör att alla användare behåller sin närvarostatus från den första inläsningen.", "Troubleshoot_Disable_Sessions_Monitor": "Inaktivera sessionsövervakning", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Med den här inställningen stoppas bearbetningen av användarsessioner, vilket gör att statistikfunktionen slutar fungera som den ska.", - "Troubleshoot_Disable_Statistics_Generator": "Inaktivera generering av statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Med den här inställningen stoppas bearbetningen av all statistik. Det gör att informationssidan blir inaktuell tills någon klickar på uppdateringsknappen och kan leda till att annan information saknas i systemet.", - "Troubleshoot_Disable_Workspace_Sync": "Inaktivera synkronisering av arbetsyta", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Den här inställningen stoppar serverns synkronisering med Rocket.Chat-molnet och kan orsaka problem med Marketplace och Enterprise-licenser.", "True": "Sant", "Try_now": "Pröva nu", "Try_searching_in_the_marketplace_instead": "Pröva att söka i Marketplace istället", @@ -5608,8 +5604,6 @@ "onboarding.page.requestTrial.subtitle": "Prova på vårt bästa Enterprise Edition-abonnemang i 30 dagar utan kostnad", "onboarding.page.magicLinkEmail.title": "Vi har skickat en inloggningslänk via e-post ", "onboarding.page.magicLinkEmail.subtitle": "Logga in på arbetsytan genom att klicka på länken i e-postmeddelandet vi precis skickade till dig. <1>Länken upphör att gälla om 30 minuter.", - "onboarding.page.organizationInfoPage.title": "Några uppgifter till...", - "onboarding.page.organizationInfoPage.subtitle": "Vi behöver dem för att anpassa arbetsytan.", "onboarding.form.adminInfoForm.title": "Information om administratör", "onboarding.form.adminInfoForm.subtitle": "Vi behöver skapa en administratörsprofil i din arbetsyta", "onboarding.form.adminInfoForm.fields.fullName.label": "Fullständigt namn", @@ -5638,12 +5632,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrering med externa leverantörer (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Åtkomst till appar i Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-postadress för molnkonto", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "För att registrera servern behöver vi ansluta den till ditt molnkonto. Om du har ett kopplar vi det automatiskt. Annars skapas ett nytt konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Ange din e-postadress", "onboarding.form.registeredServerForm.keepInformed": "Håll mig informerad om nyheter och händelser", "onboarding.form.registeredServerForm.registerLater": "Registrera dig senare", "onboarding.form.registeredServerForm.notConnectedToInternet": "Servern är inte ansluten till internet, så du måste göra en offline-registrering för den här arbetsytan.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Genom att registrera mig godkänner jag att ta emot relevanta produkt- och säkerhetsuppdateringar", "onboarding.form.standaloneServerForm.title": "Bekräftelse av fristående server", "onboarding.form.standaloneServerForm.servicesUnavailable": "Vissa av tjänsterna kommer att vara otillgängliga eller måste ställas in manuellt", "onboarding.form.standaloneServerForm.publishOwnApp": "Om du ska kunna skicka pushmeddelanden måste du kompilera och publicera din egen app på Google Play och App Store", @@ -5762,4 +5754,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} __kontext__-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", "Theme_Appearence": "Utseende för tema" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index 5ad5c0eb1d2b3..24dedfcf33e7f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -4068,10 +4068,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "這個設定可防止所有實務將使用者的狀態更改發送到其客戶端,從而使所有使用者保持首次載入的狀態!", "Troubleshoot_Disable_Sessions_Monitor": "停用 Sessions 監視器", "Troubleshoot_Disable_Sessions_Monitor_Alert": "這個設定將停止處理使用者 sessions,將會導致統計資料無法正常工作!", - "Troubleshoot_Disable_Statistics_Generator": "停用統計資料產生器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "這個設定將停止處理所有統計資料,將會使資料頁面過時,直到有人點擊“重整”按鈕,並可能導致系統周圍缺少其他資料!", - "Troubleshoot_Disable_Workspace_Sync": "停用工作區同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "這個設定將停止該伺服器與 Rocket.Chat 的雲端同步,並可能導致商店和企業授權出現問題!", "True": "是", "Try_now": "現在再試", "Tuesday": "星期二", @@ -4553,8 +4549,6 @@ "onboarding.page.requestTrial.subtitle": "試用我們最棒的企業版方案,30 天免費", "onboarding.page.magicLinkEmail.title": "我們已透過電子郵件傳送登入連結給您", "onboarding.page.magicLinkEmail.subtitle": "按一下我們剛傳送給您的電子郵件中的連結,即可登入您的工作空間。<1>該連結將在 30 分鐘後到期。", - "onboarding.page.organizationInfoPage.title": "更多詳細資料...", - "onboarding.page.organizationInfoPage.subtitle": "這些資料將可協助我們個人化您的工作空間。", "onboarding.form.adminInfoForm.title": "管理員資訊", "onboarding.form.adminInfoForm.subtitle": "我們需要此資訊以在您的工作空間內建立管理員個人資料", "onboarding.form.adminInfoForm.fields.fullName.label": "全名", @@ -4583,10 +4577,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "與外部提供者 (WhatsApp、Facebook、Telegram、Twitter) 整合", "onboarding.form.registeredServerForm.included.apps": "存取市集應用程式", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "雲端帳戶電子郵件", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "若要註冊您的伺服器,我們需要將伺服器連線至您的雲端帳戶。如果您已有雲端帳戶,我們將會自動為您連線。否則,將需要建立新的帳戶", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "請輸入您的電子郵件", "onboarding.form.registeredServerForm.keepInformed": "在有新聞與活動消息時通知我", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "註冊即代表我同意收到相關產品與安全性更新資訊", "onboarding.form.standaloneServerForm.title": "獨立伺服器確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "部分服務將無法使用或是需要手動設定", "onboarding.form.standaloneServerForm.publishOwnApp": "若要傳送推播通知,您必須對您所擁有的應用程式進行編碼,並將應用程式發佈至 Google Play 和 App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index df4642f4b0df5..2839441755414 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -3717,10 +3717,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "这个设置可以防止所有的实例将用户的状态变化发送给他们的客户端,使所有的用户保持他们第一次加载时的存在状态。", "Troubleshoot_Disable_Sessions_Monitor": "禁用会话监控", "Troubleshoot_Disable_Sessions_Monitor_Alert": "这个设置停止了对用户会话的处理,将导致统计工作无法正常进行!", - "Troubleshoot_Disable_Statistics_Generator": "禁用统计生成器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "这个设置会停止处理所有的统计数据,使信息页面过时,直到有人点击刷新按钮,并可能导致系统中的其他信息缺失!", - "Troubleshoot_Disable_Workspace_Sync": "禁用工作区同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "该设置会停止同步该服务器到 Rocket.Chat 云端,并可能导致市场和企业许可证出现问题!", "True": "是", "Try_now": "立即尝试", "Tuesday": "星期二", diff --git a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js index c283af960e673..20b26b9cdcf00 100644 --- a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js +++ b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js @@ -1,6 +1,8 @@ import { exec } from 'child_process'; import os from 'os'; import util from 'util'; +import path from 'path'; +import fs from 'fs'; const execAsync = util.promisify(exec); @@ -24,6 +26,9 @@ class VersionCompiler { }; output.marketplaceApiVersion = require('@rocket.chat/apps-engine/package.json').version.replace(/^[^0-9]/g, ''); + const minimumClientVersions = + JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }))?.rocketchat + ?.minimumClientVersions || {}; try { const result = await execAsync("git log --pretty=format:'%H%n%ad%n%an%n%s' -n 1"); const data = result.stdout.split('\n'); @@ -55,7 +60,8 @@ class VersionCompiler { // no branch } - output = `exports.Info = ${JSON.stringify(output, null, 4)};`; + output = `exports.Info = ${JSON.stringify(output, null, 4)}; + exports.minimumClientVersions = ${JSON.stringify(minimumClientVersions, null, 4)};`; file.addJavaScript({ data: output, diff --git a/apps/meteor/public/images/mentions-upsell-modal.png b/apps/meteor/public/images/mentions-upsell-modal.png new file mode 100644 index 0000000000000..a71b3b837d9a7 Binary files /dev/null and b/apps/meteor/public/images/mentions-upsell-modal.png differ diff --git a/apps/meteor/server/configureLogLevel.ts b/apps/meteor/server/configureLogLevel.ts new file mode 100644 index 0000000000000..b328d79a023aa --- /dev/null +++ b/apps/meteor/server/configureLogLevel.ts @@ -0,0 +1,8 @@ +import type { LogLevelSetting } from '@rocket.chat/logger'; +import { logLevel } from '@rocket.chat/logger'; +import { Settings } from '@rocket.chat/models'; + +const LogLevel = await Settings.getValueById('Log_Level'); +if (LogLevel) { + logLevel.emit('changed', LogLevel as LogLevelSetting); +} diff --git a/apps/meteor/server/cron/statistics.ts b/apps/meteor/server/cron/statistics.ts index b6f58b66d3835..27c1fc064e25f 100644 --- a/apps/meteor/server/cron/statistics.ts +++ b/apps/meteor/server/cron/statistics.ts @@ -32,28 +32,10 @@ async function generateStatistics(logger: Logger): Promise { } export async function statsCron(logger: Logger): Promise { - if (settings.get('Troubleshoot_Disable_Statistics_Generator')) { - return; - } - const name = 'Generate and save statistics'; + await generateStatistics(logger); - let previousValue: boolean; - settings.watch('Troubleshoot_Disable_Statistics_Generator', async (value) => { - if (value === previousValue) { - return; - } - previousValue = value; - - if (value) { - await cronJobs.remove(name); - return; - } - - await generateStatistics(logger); - - const now = new Date(); + const now = new Date(); - await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => generateStatistics(logger)); - }); + await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => generateStatistics(logger)); } diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 44302ae9ff91a..939d916616502 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -25,33 +25,21 @@ const language = settings.get('Language') || 'en'; const t = (s: string): string => i18n.t(s, { lng: language }); async function getGuestByEmail(email: string, name: string, department = ''): Promise { - logger.debug(`Attempt to register a guest for ${email} on department: ${department}`); const guest = await LivechatVisitors.findOneGuestByEmailAddress(email); if (guest) { - logger.debug(`Guest with email ${email} found with id ${guest._id}`); if (guest.department !== department) { - logger.debug({ - msg: 'Switching departments for guest', - guest, - previousDepartment: guest.department, - newDepartment: department, - }); if (!department) { await LivechatVisitors.removeDepartmentById(guest._id); delete guest.department; return guest; } await LivechatTyped.setDepartmentForGuest({ token: guest.token, department }); - return LivechatVisitors.findOneById(guest._id, {}); + return LivechatVisitors.findOneEnabledById(guest._id, {}); } return guest; } - logger.debug({ - msg: 'Creating a new Omnichannel guest for visitor with email', - email, - }); const userId = await LivechatTyped.registerGuest({ token: Random.id(), name: name || email, @@ -59,8 +47,9 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr department, }); - const newGuest = await LivechatVisitors.findOneById(userId); + const newGuest = await LivechatVisitors.findOneEnabledById(userId); logger.debug(`Guest ${userId} for visitor ${email} created`); + if (newGuest) { return newGuest; } @@ -111,7 +100,7 @@ async function uploadAttachment(attachmentParam: Attachment, rid: string, visito } export async function onEmailReceived(email: ParsedMail, inbox: string, department = ''): Promise { - logger.debug(`New email conversation received on inbox ${inbox}. Will be assigned to department ${department}`); + logger.info(`New email conversation received on inbox ${inbox}. Will be assigned to department ${department}`); if (!email.from?.value?.[0]?.address) { return; } @@ -119,19 +108,13 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme const references = typeof email.references === 'string' ? [email.references] : email.references; const initialRef = [email.messageId, email.inReplyTo].filter(Boolean) as string[]; const thread = (references?.length ? references : []).flatMap((t: string) => t.split(',')).concat(initialRef); - - logger.debug(`Received new email conversation with thread ${thread} on inbox ${inbox} from ${email.from.value[0].address}`); - - logger.debug(`Fetching guest for visitor ${email.from.value[0].address}`); const guest = await getGuestByEmail(email.from.value[0].address, email.from.value[0].name, department); if (!guest) { - logger.debug(`No visitor found for ${email.from.value[0].address}`); + logger.error(`No visitor found for ${email.from.value[0].address}`); return; } - logger.debug(`Guest ${guest._id} obtained. Attempting to find or create a room on department ${department}`); - let room: IOmnichannelRoom | null = await LivechatRooms.findOneByVisitorTokenAndEmailThreadAndDepartment( guest.token, thread, @@ -146,7 +129,6 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme }); if (room?.closedAt) { - logger.debug(`Room ${room?._id} is closed. Reopening`); room = await QueueManager.unarchiveRoom(room); } @@ -166,8 +148,6 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme const rid = room?._id ?? Random.id(); const msgId = Random.id(); - logger.debug(`Sending email message to room ${rid} for visitor ${guest._id}. Conversation assigned to department ${department}`); - Livechat.sendMessage({ guest, message: { @@ -242,7 +222,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme try { attachments.push(await uploadAttachment(attachment, rid, guest.token)); } catch (err) { - Livechat.logger.error({ msg: 'Error uploading attachment from email', err }); + logger.error({ msg: 'Error uploading attachment from email', err }); } } @@ -259,7 +239,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme room && (await LivechatRooms.updateEmailThreadByRoomId(room._id, thread)); }) .catch((err) => { - Livechat.logger.error({ + logger.error({ msg: 'Error receiving email', err, }); diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index dccf8315acd33..685c7f9e96dd8 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -43,15 +43,15 @@ const sendErrorReplyMessage = async (error: string, options: any) => { return sendMessage(user, message, { _id: options.rid }); }; -const sendSuccessReplyMessage = async (options: any) => { - if (!options?.rid || !options?.msgId) { +const sendSuccessReplyMessage = async (options: { room: IOmnichannelRoom; msgId: string; sender: string }) => { + if (!options?.room?._id || !options?.msgId) { return; } const message = { groupable: false, msg: `@${options.sender} Attachment was sent successfully`, _id: String(Date.now()), - rid: options.rid, + rid: options.room._id, ts: new Date(), }; @@ -60,7 +60,7 @@ const sendSuccessReplyMessage = async (options: any) => { return; } - return sendMessage(user, message, { _id: options.rid }); + return sendMessage(user, message, options.room); }; async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promise<{ messageId: string }> { @@ -75,7 +75,7 @@ async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promi ...mail, }) .then((info) => { - logger.info('Message sent: %s', info.messageId); + logger.info({ msg: 'Message sent', info }); return info; }) .catch(async (err) => { @@ -92,7 +92,6 @@ async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promi slashCommands.add({ command: 'sendEmailAttachment', callback: async ({ command, params }: SlashCommandCallbackParams<'sendEmailAttachment'>) => { - logger.debug('sendEmailAttachment command: ', command, params); if (command !== 'sendEmailAttachment' || !Match.test(params, String)) { return; } @@ -175,7 +174,7 @@ slashCommands.add({ return sendSuccessReplyMessage({ msgId: message._id, sender: message.u.username, - rid: room._id, + room, }); }, options: { @@ -318,7 +317,6 @@ export async function sendTestEmailToInbox(emailInboxRecord: IEmailInbox, user: throw new Error('user-without-verified-email'); } - logger.info(`Sending testing email to ${address}`); void sendEmail(inbox, { to: address, subject: 'Test of inbox configuration', diff --git a/apps/meteor/server/lib/dataExport/sendFile.ts b/apps/meteor/server/lib/dataExport/sendFile.ts index c4f75bd48bffd..54c691127d9a1 100644 --- a/apps/meteor/server/lib/dataExport/sendFile.ts +++ b/apps/meteor/server/lib/dataExport/sendFile.ts @@ -64,9 +64,11 @@ export const sendFile = async (data: ExportFile, user: IUser): Promise => await exportMessages(); + const promises: Promise[] = []; for await (const attachmentData of fullFileList) { - await copyFileUpload(attachmentData, assetsPath); + promises.push(copyFileUpload(attachmentData, assetsPath)); } + await Promise.all(promises); const exportFile = `${baseDir}-export.zip`; await makeZipFile(exportPath, exportFile); diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts new file mode 100644 index 0000000000000..92e76d8c2ec13 --- /dev/null +++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts @@ -0,0 +1,23 @@ +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; + +import { settings } from '../../app/settings/server'; + +export function getSubscriptionAutotranslateDefaultConfig(user: AtLeast): + | { + autoTranslate: boolean; + autoTranslateLanguage: string; + } + | undefined { + if (!settings.get('AutoTranslate_AutoEnableOnJoinRoom')) { + return; + } + + const languageSetting = settings.get('Language'); + + const { language: userLanguage } = user.settings?.preferences || {}; + if (!userLanguage || userLanguage === 'default' || languageSetting === userLanguage) { + return; + } + + return { autoTranslate: true, autoTranslateLanguage: userLanguage }; +} diff --git a/apps/meteor/server/lib/migrations.ts b/apps/meteor/server/lib/migrations.ts index da3aeec761e6a..f70b5bcca9ffd 100644 --- a/apps/meteor/server/lib/migrations.ts +++ b/apps/meteor/server/lib/migrations.ts @@ -292,9 +292,24 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma return true; } -export const onFreshInstall = - (await getControl()).version !== 0 - ? async (): Promise => { - /* noop */ - } - : (fn: () => unknown): unknown => fn(); +export async function onServerVersionChange(cb: () => Promise): Promise { + const result = await Migrations.findOneAndUpdate( + { + _id: 'upgrade', + }, + { + $set: { + hash: Info.commit.hash, + }, + }, + { + upsert: true, + }, + ); + + if (result.value?.hash === Info.commit.hash) { + return; + } + + await cb(); +} diff --git a/apps/meteor/server/lib/roles/addUserRoles.ts b/apps/meteor/server/lib/roles/addUserRoles.ts index 395056903ae4a..a064553f5cb4f 100644 --- a/apps/meteor/server/lib/roles/addUserRoles.ts +++ b/apps/meteor/server/lib/roles/addUserRoles.ts @@ -1,6 +1,6 @@ import { MeteorError } from '@rocket.chat/core-services'; import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; -import { Users, Roles } from '@rocket.chat/models'; +import { Roles } from '@rocket.chat/models'; import { validateRoleList } from './validateRoleList'; @@ -9,11 +9,6 @@ export const addUserRolesAsync = async (userId: IUser['_id'], roleIds: IRole['_i return false; } - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - if (!user) { - throw new MeteorError('error-invalid-user', 'Invalid user'); - } - if (!(await validateRoleList(roleIds))) { throw new MeteorError('error-invalid-role', 'Invalid role'); } diff --git a/apps/meteor/server/lib/rooms/roomTypes/direct.ts b/apps/meteor/server/lib/rooms/roomTypes/direct.ts index b18418258cb3c..ad1913345b851 100644 --- a/apps/meteor/server/lib/rooms/roomTypes/direct.ts +++ b/apps/meteor/server/lib/rooms/roomTypes/direct.ts @@ -1,4 +1,4 @@ -import type { IRoom, AtLeast } from '@rocket.chat/core-typings'; +import type { AtLeast } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -42,7 +42,7 @@ roomCoordinator.add(DirectMessageRoomType, { } }, - async allowMemberAction(room: IRoom, action, userId) { + async allowMemberAction(room, action, userId) { if (isRoomFederated(room)) { return Federation.actionAllowed(room, action, userId); } diff --git a/apps/meteor/server/lib/rooms/roomTypes/livechat.ts b/apps/meteor/server/lib/rooms/roomTypes/livechat.ts index 88393088541eb..92d722ac2bb09 100644 --- a/apps/meteor/server/lib/rooms/roomTypes/livechat.ts +++ b/apps/meteor/server/lib/rooms/roomTypes/livechat.ts @@ -39,7 +39,7 @@ roomCoordinator.add(LivechatRoomType, { }, async getMsgSender(senderId) { - return LivechatVisitors.findOneById(senderId); + return LivechatVisitors.findOneEnabledById(senderId); }, getReadReceiptsExtraData(message) { diff --git a/apps/meteor/server/lib/spotlight.js b/apps/meteor/server/lib/spotlight.js index 38dc1b8738780..62fcdc3a66b40 100644 --- a/apps/meteor/server/lib/spotlight.js +++ b/apps/meteor/server/lib/spotlight.js @@ -1,3 +1,4 @@ +import { Team } from '@rocket.chat/core-services'; import { Users, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -133,8 +134,31 @@ export class Spotlight { } } - async _performExtraUserSearches(/* userId, searchParams */) { - // Overwrite this method to include extra searches + mapTeams(teams) { + return teams.map((t) => { + t.isTeam = true; + t.username = t.name; + t.status = 'online'; + return t; + }); + } + + async _searchTeams(userId, { text, options, users, mentions }) { + if (!mentions || settings.get('Troubleshoot_Disable_Teams_Mention')) { + return users; + } + + options.limit -= users.length; + + if (options.limit <= 0) { + return users; + } + + const teamOptions = { ...options, projection: { name: 1, type: 1 } }; + const teams = await Team.search(userId, text, teamOptions); + users.push(...this.mapTeams(teams)); + + return users; } async searchUsers({ userId, rid, text, usernames, mentions }) { @@ -245,7 +269,7 @@ export class Spotlight { return users; } - if (await this._performExtraUserSearches(userId, searchParams)) { + if (await this._searchTeams(userId, searchParams)) { return users; } diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 5579261911f56..b9418fe438302 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -1,4 +1,5 @@ import './models/startup'; +import './configureLogLevel'; import './settings/index'; import '../ee/server/models/startup'; import './services/startup'; @@ -9,9 +10,10 @@ import './importPackages'; import '../imports/startup/server'; import '../app/lib/server/startup'; +import '../ee/server/startup'; +import './startup'; import '../ee/server'; import './lib/pushConfig'; -import './startup'; import './configuration/accounts_meld'; import './configuration/ldap'; import './methods/OEmbedCacheCleanup'; diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index cbbafccfe60ba..11232908b8470 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; import { callbacks } from '../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../lib/getSubscriptionAutotranslateDefaultConfig'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,6 +56,7 @@ Meteor.methods({ continue; } await callbacks.run('beforeJoinRoom', user, room); + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, @@ -62,6 +64,7 @@ Meteor.methods({ unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now }); await callbacks.run('afterJoinRoom', user, room); diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts index d92c7e46292e6..ccbfe8916cae5 100644 --- a/apps/meteor/server/methods/createDirectMessage.ts +++ b/apps/meteor/server/methods/createDirectMessage.ts @@ -104,7 +104,11 @@ export async function createDirectMessage( } catch (error) { throw new Meteor.Error((error as any)?.message); } - const { _id: rid, inserted, ...room } = await createRoom('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options); + const { + _id: rid, + inserted, + ...room + } = await createRoom<'d'>('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options); return { // @ts-expect-error - room type is already defined in the `createRoom` return type diff --git a/apps/meteor/server/methods/getPasswordPolicy.ts b/apps/meteor/server/methods/getPasswordPolicy.ts index 7201999ff677f..cc35f1cfb5146 100644 --- a/apps/meteor/server/methods/getPasswordPolicy.ts +++ b/apps/meteor/server/methods/getPasswordPolicy.ts @@ -28,6 +28,9 @@ Meteor.methods({ method: 'getPasswordPolicy', }); } - return passwordPolicy.getPasswordPolicy(); + return passwordPolicy.getPasswordPolicy() as { + enabled: boolean; + policy: [name: TranslationKey, options?: Record][]; + }; }, }); diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index ea5bfa9edcff4..2f29b1f55039a 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { getUsersInRole } from '../../app/authorization/server'; +import { canAccessRoomAsync, getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../app/authorization/server/functions/hasRole'; import { RoomMemberActions } from '../../definition/IRoomTypeConfig'; @@ -35,8 +35,6 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri }); } - const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); - const fromUser = await Users.findOneById(fromId); if (!fromUser) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -44,13 +42,25 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { - projection: { _id: 1 }, - }); - if (!subscription) { - throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { - method: 'removeUserFromRoom', + // did this way so a ctrl-f would find the permission being used + const kickAnyUserPermission = room.t === 'c' ? 'kick-user-from-any-c-room' : 'kick-user-from-any-p-room'; + + const canKickAnyUser = await hasPermissionAsync(fromId, kickAnyUserPermission); + if (!canKickAnyUser && !(await canAccessRoomAsync(room, fromUser))) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); + + if (!canKickAnyUser) { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { + projection: { _id: 1 }, }); + if (!subscription) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeUserFromRoom', + }); + } } if (await hasRoleAsync(removedUser._id, 'owner', room._id)) { diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index 731b34abf283d..71abe7bea3b1e 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -41,6 +41,7 @@ type UserPreferences = { receiveLoginDetectionEmail: boolean; notifyCalendarEvents: boolean; enableMobileRinging: boolean; + mentionsWithSymbol?: boolean; }; declare module '@rocket.chat/ui-contexts' { @@ -87,6 +88,7 @@ export const saveUserPreferences = async (settings: Partial, us omnichannelTranscriptPDF: Match.Optional(Boolean), notifyCalendarEvents: Match.Optional(Boolean), enableMobileRinging: Match.Optional(Boolean), + mentionsWithSymbol: Match.Optional(Boolean), }; check(settings, Match.ObjectIncluding(keys)); const user = await Users.findOneById(userId); diff --git a/apps/meteor/server/models/CloudAnnouncements.ts b/apps/meteor/server/models/CloudAnnouncements.ts new file mode 100644 index 0000000000000..4f6692d67fc9e --- /dev/null +++ b/apps/meteor/server/models/CloudAnnouncements.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { CloudAnnouncementsRaw } from './raw/CloudAnnouncements'; + +registerModel('ICloudAnnouncementsModel', new CloudAnnouncementsRaw(db)); diff --git a/apps/meteor/server/models/raw/CloudAnnouncements.ts b/apps/meteor/server/models/raw/CloudAnnouncements.ts new file mode 100644 index 0000000000000..21b4304b2bd5e --- /dev/null +++ b/apps/meteor/server/models/raw/CloudAnnouncements.ts @@ -0,0 +1,11 @@ +import type { Cloud } from '@rocket.chat/core-typings'; +import type { ICloudAnnouncementsModel } from '@rocket.chat/model-typings'; +import type { Db } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class CloudAnnouncementsRaw extends BaseRaw implements ICloudAnnouncementsModel { + constructor(db: Db) { + super(db, 'cloud_announcements'); + } +} diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 1687c0d5cb87c..974c2b5cb570e 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -8,6 +8,7 @@ import type { ILivechatPriority, IOmnichannelServiceLevelAgreements, ReportResult, + MACStats, } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; @@ -74,6 +75,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { key: { departmentId: 1, ts: 1 }, partialFilterExpression: { departmentId: { $exists: true }, t: 'l' } }, { key: { 'tags.0': 1, 'ts': 1 }, partialFilterExpression: { 'tags.0': { $exists: true }, 't': 'l' } }, { key: { servedBy: 1, ts: 1 }, partialFilterExpression: { servedBy: { $exists: true }, t: 'l' } }, + { key: { 'v.activity': 1, 'ts': 1 }, partialFilterExpression: { 'v.activity': { $exists: true }, 't': 'l' } }, ]; } @@ -1516,11 +1518,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $set: { pdfTranscriptRequested: true }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } @@ -1532,11 +1529,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $unset: { pdfTranscriptRequested: 1 }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } @@ -1548,11 +1540,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $set: { pdfTranscriptFileId: fileId }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } @@ -1986,7 +1973,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.find(query, options); } - setResponseByRoomId(roomId: string, response: { user: { _id: string; username: string } }) { + setResponseByRoomId(roomId: string, responseBy: IOmnichannelRoom['responseBy']) { return this.updateOne( { _id: roomId, @@ -1994,11 +1981,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }, { $set: { - responseBy: { - _id: response.user._id, - username: response.user.username, - lastMessageTs: new Date(), - }, + responseBy, }, $unset: { waitingResponse: 1, @@ -2452,6 +2435,140 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.updateOne(query, update); } + markVisitorActiveForPeriod(rid: string, period: string): Promise { + const query = { + _id: rid, + }; + + const update = { + $addToSet: { + 'v.activity': period, + }, + }; + + return this.updateOne(query, update); + } + + async getMACStatisticsForPeriod(period: string): Promise { + return this.col + .aggregate([ + { + $match: { + 't': 'l', + 'v.activity': period, + }, + }, + { + $group: { + _id: { + source: { + $ifNull: ['$source.alias', '$source.type'], + }, + }, + contactsCount: { + $addToSet: '$v._id', + }, + conversationsCount: { + $sum: 1, + }, + }, + }, + { + $group: { + _id: null, + sources: { + $push: { + source: '$_id.source', + contactsCount: { + $size: '$contactsCount', + }, + conversationsCount: '$conversationsCount', + }, + }, + totalContactsCount: { + $sum: { + $size: '$contactsCount', + }, + }, + totalConversationsCount: { + $sum: '$conversationsCount', + }, + }, + }, + { + $project: { + _id: 0, + contactsCount: '$totalContactsCount', + conversationsCount: '$totalConversationsCount', + sources: 1, + }, + }, + ]) + .toArray(); + } + + async getMACStatisticsBetweenDates(start: Date, end: Date): Promise { + return this.col + .aggregate([ + { + $match: { + 't': 'l', + 'v.activity': { $exists: true }, + 'ts': { + $gte: start, + $lt: end, + }, + }, + }, + { + $group: { + _id: { + source: { + $ifNull: ['$source.alias', '$source.type'], + }, + }, + contactsCount: { + $addToSet: '$v._id', + }, + conversationsCount: { + $sum: 1, + }, + }, + }, + { + $group: { + _id: null, + sources: { + $push: { + source: '$_id.source', + contactsCount: { + $size: '$contactsCount', + }, + conversationsCount: '$conversationsCount', + }, + }, + totalContactsCount: { + $sum: { + $size: '$contactsCount', + }, + }, + totalConversationsCount: { + $sum: '$conversationsCount', + }, + }, + }, + { + $project: { + _id: 0, + contactsCount: '$totalContactsCount', + conversationsCount: '$totalConversationsCount', + sources: 1, + }, + }, + ]) + .toArray(); + } + async unsetAllPredictedVisitorAbandonment(): Promise { throw new Error('Method not implemented.'); } diff --git a/apps/meteor/server/models/raw/LivechatVisitors.ts b/apps/meteor/server/models/raw/LivechatVisitors.ts index 2df2ae09882bf..7b478bab43d6d 100644 --- a/apps/meteor/server/models/raw/LivechatVisitors.ts +++ b/apps/meteor/server/models/raw/LivechatVisitors.ts @@ -32,6 +32,8 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL { key: { username: 1 } }, { key: { 'contactMananger.username': 1 }, sparse: true }, { key: { 'livechatData.$**': 1 } }, + { key: { activity: 1 }, partialFilterExpression: { activity: { $exists: true } } }, + { key: { disabled: 1 }, partialFilterExpression: { disabled: { $exists: true } } }, ]; } @@ -63,9 +65,29 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL return this.find(query, options); } + findEnabled(query: Filter, options?: FindOptions): FindCursor { + return this.find( + { + ...query, + disabled: { $ne: true }, + }, + options, + ); + } + + findOneEnabledById(_id: string, options?: FindOptions): Promise { + const query = { + _id, + disabled: { $ne: true }, + }; + + return this.findOne(query, options); + } + findVisitorByToken(token: string): FindCursor { const query = { token, + disabled: { $ne: true }, }; return this.find(query); @@ -81,6 +103,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL getVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department?: string }): FindCursor { const query = { + disabled: { $ne: true }, _updatedAt: { $gte: new Date(start), $lt: new Date(end), @@ -166,7 +189,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL options?: FindOptions, ): Promise>> { if (!emailOrPhone && !nameOrUsername && allowedCustomFields.length === 0) { - return this.findPaginated({}, options); + return this.findPaginated({ disabled: { $ne: true } }, options); } const query: Filter = { @@ -193,6 +216,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL : []), ...allowedCustomFields.map((c: string) => ({ [`livechatData.${c}`]: nameOrUsername })), ], + disabled: { $ne: true }, }; return this.findPaginated(query, options); @@ -204,7 +228,9 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL customFields?: { [key: string]: RegExp }, ): Promise { const query = Object.assign( - {}, + { + disabled: { $ne: true }, + }, { ...(email && { visitorEmails: { address: email } }), ...(phone && { phone: { phoneNumber: phone } }), @@ -212,7 +238,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL }, ); - if (Object.keys(query).length === 0) { + if (Object.keys(query).length === 1) { return null; } @@ -365,6 +391,60 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL }, ); } + + isVisitorActiveOnPeriod(visitorId: string, period: string): Promise { + const query = { + _id: visitorId, + activity: period, + }; + + return this.findOne(query, { projection: { _id: 1 } }).then(Boolean); + } + + markVisitorActiveForPeriod(visitorId: string, period: string): Promise { + const query = { + _id: visitorId, + }; + + const update = { + $push: { + activity: { + $each: [period], + $slice: -12, + }, + }, + }; + + return this.updateOne(query, update); + } + + disableById(_id: string): Promise { + return this.updateOne( + { _id }, + { + $set: { disabled: true }, + $unset: { + department: 1, + contactManager: 1, + token: 1, + visitorEmails: 1, + phone: 1, + name: 1, + livechatData: 1, + lastChat: 1, + ip: 1, + host: 1, + userAgent: 1, + }, + }, + ); + } + + countVisitorsOnPeriod(period: string): Promise { + return this.countDocuments({ + activity: period, + }); + } } type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 673fd83f78009..8b8a6637284c8 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -678,6 +678,16 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ); } + findE2ERoomById(roomId: IRoom['_id'], options: FindOptions = {}): Promise { + return this.findOne( + { + _id: roomId, + encrypted: true, + }, + options, + ); + } + findRoomsInsideTeams(autoJoin = false): FindCursor { return this.find({ teamId: { $exists: true }, diff --git a/apps/meteor/server/models/raw/Sessions.ts b/apps/meteor/server/models/raw/Sessions.ts index 68d5149232ed1..c02fc8b5de99c 100644 --- a/apps/meteor/server/models/raw/Sessions.ts +++ b/apps/meteor/server/models/raw/Sessions.ts @@ -167,9 +167,9 @@ const getProjectionByFullDate = (): { day: string; month: string; year: string } }); export const aggregates = { - dailySessionsOfYesterday( + dailySessions( collection: Collection, - { year, month, day }: DestructuredDate, + { start, end }: DestructuredRange, ): AggregationCursor< Pick & { time: number; @@ -178,115 +178,101 @@ export const aggregates = { _computedAt: string; } > { - return collection.aggregate< - Pick & { - time: number; - sessions: number; - devices: ISession['device'][]; - _computedAt: string; - } - >( - [ - { - $match: { - userId: { $exists: true }, - lastActivityAt: { $exists: true }, - device: { $exists: true }, - type: 'session', - $or: [ - { - year: { $lt: year }, - }, - { - year, - month: { $lt: month }, - }, - { - year, - month, - day: { $lte: day }, - }, - ], - }, - }, - { - $project: { - userId: 1, - device: 1, - day: 1, - month: 1, - year: 1, - mostImportantRole: 1, - time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } }, - }, - }, - { - $match: { - time: { $gt: 0 }, - }, + const pipeline = [ + { + $match: { + userId: { $exists: true }, + lastActivityAt: { $exists: true }, + device: { $exists: true }, + type: 'session', + ...matchBasedOnDate(start, end), }, - { - $group: { - _id: { - userId: '$userId', - device: '$device', - day: '$day', - month: '$month', - year: '$year', - }, - mostImportantRole: { $first: '$mostImportantRole' }, - time: { $sum: '$time' }, - sessions: { $sum: 1 }, - }, + }, + { + $project: { + userId: 1, + device: 1, + day: 1, + month: 1, + year: 1, + mostImportantRole: 1, + time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } }, }, - { - $sort: { - time: -1, - }, + }, + { + $match: { + time: { $gt: 0 }, }, - { - $group: { - _id: { - userId: '$_id.userId', - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', - }, - mostImportantRole: { $first: '$mostImportantRole' }, - time: { $sum: '$time' }, - sessions: { $sum: '$sessions' }, - devices: { - $push: { - sessions: '$sessions', - time: '$time', - device: '$_id.device', - }, - }, + }, + { + $group: { + _id: { + userId: '$userId', + device: '$device', + day: '$day', + month: '$month', + year: '$year', }, + mostImportantRole: { $first: '$mostImportantRole' }, + time: { $sum: '$time' }, + sessions: { $sum: 1 }, }, - { - $sort: { - _id: 1, - }, + }, + { + $sort: { + time: -1, }, - { - $project: { - _id: 0, - type: { $literal: 'user_daily' }, - _computedAt: { $literal: new Date() }, + }, + { + $group: { + _id: { + userId: '$_id.userId', day: '$_id.day', month: '$_id.month', year: '$_id.year', - userId: '$_id.userId', - mostImportantRole: 1, - time: 1, - sessions: 1, - devices: 1, + }, + mostImportantRole: { $first: '$mostImportantRole' }, + time: { $sum: '$time' }, + sessions: { $sum: '$sessions' }, + devices: { + $push: { + sessions: '$sessions', + time: '$time', + device: '$_id.device', + }, }, }, - ], - { allowDiskUse: true }, - ); + }, + { + $sort: { + _id: 1, + }, + }, + { + $project: { + _id: 0, + type: { $literal: 'user_daily' }, + _computedAt: { $literal: new Date() }, + day: '$_id.day', + month: '$_id.month', + year: '$_id.year', + userId: '$_id.userId', + mostImportantRole: 1, + time: 1, + sessions: 1, + devices: 1, + }, + }, + ]; + + return collection.aggregate< + Pick & { + time: number; + sessions: number; + devices: ISession['device'][]; + _computedAt: string; + } + >(pipeline, { allowDiskUse: true }); }, async getUniqueUsersOfYesterday( @@ -1616,4 +1602,23 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { return this.col.bulkWrite(ops, { ordered: false }); } + + async updateDailySessionById(_id: ISession['_id'], record: Partial): Promise { + return this.updateOne({ _id }, { $set: record }, { upsert: true }); + } + + async updateAllSessionsByDateToComputed({ start, end }: DestructuredRange): Promise { + return this.updateMany( + { + type: 'session', + ...matchBasedOnDate(start, end), + }, + { + $set: { + type: 'computed-session', + _computedAt: new Date(), + }, + }, + ); + } } diff --git a/apps/meteor/server/models/raw/Settings.ts b/apps/meteor/server/models/raw/Settings.ts index 3a5d150c01585..1154b7dfe630c 100644 --- a/apps/meteor/server/models/raw/Settings.ts +++ b/apps/meteor/server/models/raw/Settings.ts @@ -69,6 +69,25 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { return this.updateOne(query, update); } + async resetValueById( + _id: string, + value?: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, + ): Promise { + if (value == null) { + const record = await this.findOneById(_id); + if (record) { + const prop = record.valueSource || 'packageValue'; + value = record[prop]; + } + } + + if (value == null) { + return; + } + + return this.updateValueById(_id, value); + } + async incrementValueById(_id: ISetting['_id'], value = 1): Promise { return this.updateOne( { diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 4b42367bad05e..a7b9bb347511d 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -1,4 +1,13 @@ -import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; +import type { + AtLeast, + IRole, + IRoom, + ISubscription, + IUser, + RocketChatRecordDeleted, + RoomType, + SpotlightUser, +} from '@rocket.chat/core-typings'; import type { ISubscriptionsModel } from '@rocket.chat/model-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -17,6 +26,7 @@ import type { IndexDescription, UpdateFilter, InsertOneResult, + InsertManyResult, } from 'mongodb'; import { getDefaultSubscriptionPref } from '../../../app/utils/lib/getDefaultSubscriptionPref'; @@ -594,6 +604,14 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise { + const query = { + rid: roomId, + }; + + return this.updateMany(query, { $unset: { autoTranslate: 1 } }); + } + updateAutoTranslateLanguageById(_id: string, autoTranslateLanguage: string): Promise { const query = { _id, @@ -1605,6 +1623,38 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result; } + async createWithRoomAndManyUsers( + room: IRoom, + users: { user: AtLeast; extraData: Record }[] = [], + ): Promise> { + const subscriptions = users.map(({ user, extraData }) => ({ + open: false, + alert: false, + unread: 0, + userMentions: 0, + groupMentions: 0, + ts: room.ts, + rid: room._id, + name: room.name, + fname: room.fname, + ...(room.customFields && { customFields: room.customFields }), + t: room.t, + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + ...(room.prid && { prid: room.prid }), + ...getDefaultSubscriptionPref(user), + ...extraData, + })); + + // @ts-expect-error - types not good :( + const result = await this.insertMany(subscriptions); + + return result; + } + // REMOVE async removeByUserId(userId: string): Promise { const query = { diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index dc3441092892f..c8cee8f2f6bf9 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1,3 +1,4 @@ +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -6,6 +7,8 @@ import { BaseRaw } from './BaseRaw'; const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ statusLivechat: 'available', roles: 'livechat-agent', + // ignore deactivated users + active: true, ...(!isLivechatEnabledWhenAgentIdle && { $or: [ { @@ -381,6 +384,10 @@ export class UsersRaw extends BaseRaw { } findOneByUsernameIgnoringCase(username, options) { + if (!username) { + throw new Error('invalid username'); + } + const query = { username }; return this.findOne(query, { @@ -933,7 +940,7 @@ export class UsersRaw extends BaseRaw { }, }; - return this.updateMany(query, update); + return this.updateOne(query, update); } addBusinessHourByAgentIds(agentIds = [], businessHourId) { @@ -1031,6 +1038,8 @@ export class UsersRaw extends BaseRaw { const query = { $or: [{ openBusinessHours: { $exists: false } }, { openBusinessHours: { $size: 0 } }], roles: 'livechat-agent', + // exclude deactivated users + active: true, // Avoid unnecessary updates statusLivechat: 'available', ...(Array.isArray(userIds) && userIds.length > 0 && { _id: { $in: userIds } }), @@ -1483,6 +1492,18 @@ export class UsersRaw extends BaseRaw { ); } + addRoomByUserIds(uids, rid) { + return this.updateMany( + { + _id: { $in: uids }, + __rooms: { $ne: rid }, + }, + { + $addToSet: { __rooms: rid }, + }, + ); + } + removeRoomByRoomIds(rids) { return this.updateMany( { @@ -1687,6 +1708,24 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + makeAgentUnavailableAndUnsetExtension(userId) { + const query = { + _id: userId, + roles: 'livechat-agent', + }; + + const update = { + $set: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + }, + $unset: { + extension: 1, + }, + }; + + return this.updateOne(query, update); + } + setLivechatData(userId, data = {}) { // TODO: Create class Agent const query = { @@ -2148,7 +2187,6 @@ export class UsersRaw extends BaseRaw { { active: true, type: { $nin: ['app'] }, - roles: { $ne: ['guest'] }, _id: { $in: ids }, }, options, diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 14b26e0f188fa..d355d1febd162 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -68,3 +68,4 @@ import './Imports'; import './AppsTokens'; import './CronHistory'; import './Migrations'; +import './CloudAnnouncements'; diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 973b542bf54a9..f21081e43d0a1 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -2,8 +2,9 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; -import { UserStatus, isSettingColor } from '@rocket.chat/core-typings'; +import { UserStatus, isSettingColor, isSettingEnterprise } from '@rocket.chat/core-typings'; import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; import { parse } from '@rocket.chat/message-parser'; import { settings } from '../../../app/settings/server/cached'; @@ -26,6 +27,8 @@ const minimongoChangeMap: Record = { export class ListenersModule { constructor(service: IServiceClass, notifications: NotificationsModule) { + const logger = new Logger('ListenersModule'); + service.onEvent('emoji.deleteCustom', (emoji) => { notifications.notifyLoggedInThisInstance('deleteEmojiCustom', { emojiData: emoji, @@ -247,11 +250,16 @@ export class ListenersModule { }); service.onEvent('watch.settings', async ({ clientAction, setting }): Promise => { - if (clientAction !== 'removed') { - // TODO check if setting is EE before calling this - const result = await EnterpriseSettings.changeSettingValue(setting); - if (result !== undefined && !(result instanceof Error)) { - setting.value = result; + // if a EE setting changed make sure we broadcast the correct value according to license + if (clientAction !== 'removed' && isSettingEnterprise(setting)) { + try { + const result = await EnterpriseSettings.changeSettingValue(setting); + if (result !== undefined && !(result instanceof Error)) { + setting.value = result; + } + } catch (err: unknown) { + logger.error({ msg: 'Error getting proper enterprise setting value. Returning `invalidValue` instead.', err }); + setting.value = setting.invalidValue; } } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 99863305f7c12..6918d40af8717 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -39,7 +39,7 @@ export class Authorization extends ServiceClass implements IAuthorization { } async started(): Promise { - if (!(await License.isEnterprise())) { + if (!(await License.hasValidLicense())) { return; } diff --git a/apps/meteor/server/services/banner/service.ts b/apps/meteor/server/services/banner/service.ts index 4dc0dbbec4941..d20b9e780875c 100644 --- a/apps/meteor/server/services/banner/service.ts +++ b/apps/meteor/server/services/banner/service.ts @@ -26,7 +26,7 @@ export class BannerService extends ServiceClassInternal implements IBannerServic return true; } - async create(doc: Optional): Promise { + async create(doc: Optional): Promise { const bannerId = doc._id || uuidv4(); doc.view.appId = 'banner-core'; diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts index 018a5f87704cd..c4aee8bcf2aa7 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts @@ -58,7 +58,12 @@ export class RocketChatRoomAdapter { .trim() .replace(/ /g, '-'), ); - const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, usernameOrId); + const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId); + if (!owner) { + throw new Error('Cannot create a room without a creator'); + } + + const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, owner); const roomId = rid || _id; await MatrixBridgedRoom.createOrUpdateByLocalRoomId( roomId, @@ -90,10 +95,16 @@ export class RocketChatRoomAdapter { const readonly = false; const excludeSelf = false; const extraData = undefined; + + const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId); + if (!owner) { + throw new Error('Cannot create a room without a creator'); + } + const { rid, _id } = await createRoom( federatedRoom.getRoomType(), federatedRoom.getDisplayName(), - usernameOrId, + owner, federatedRoom.getMembersUsernames(), excludeSelf, readonly, diff --git a/apps/meteor/server/services/messages/hooks/badwords.ts b/apps/meteor/server/services/messages/hooks/badwords.ts new file mode 100644 index 0000000000000..17641d8f9c7f0 --- /dev/null +++ b/apps/meteor/server/services/messages/hooks/badwords.ts @@ -0,0 +1,26 @@ +export async function configureBadWords(badWordsList?: string, goodWordsList?: string) { + const { default: Filter } = await import('bad-words'); + + 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 badWords = new Filter(options); + + if (goodWordsList?.length) { + badWords.removeWords(...goodWordsList.split(',').map((word) => word.trim())); + } + + return badWords; +} diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 48f7ad42276cf..f981422727f78 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -2,6 +2,7 @@ import type { IMessageService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IMessage, MessageTypesValues, IUser, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; +import type BadWordsFilter from 'bad-words'; import { deleteMessage } from '../../../app/lib/server/functions/deleteMessage'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; @@ -9,10 +10,30 @@ import { updateMessage } from '../../../app/lib/server/functions/updateMessage'; import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; import { executeSetReaction } from '../../../app/reactions/server/setReaction'; import { settings } from '../../../app/settings/server'; +import { configureBadWords } from './hooks/badwords'; export class MessageService extends ServiceClassInternal implements IMessageService { protected name = 'message'; + private badWordsFilter?: BadWordsFilter; + + async created() { + await this.configureBadWords(); + } + + private async configureBadWords() { + settings.watchMultiple( + ['Message_AllowBadWordsFilter', 'Message_BadWordsFilterList', 'Message_BadWordsWhitelist'], + async ([enabled, badWordsList, whiteList]) => { + if (!enabled) { + this.badWordsFilter = undefined; + return; + } + this.badWordsFilter = await configureBadWords(badWordsList as string, whiteList as string); + }, + ); + } + async sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise { return executeSendMessage(fromId, { rid, msg }); } @@ -55,4 +76,61 @@ export class MessageService extends ServiceClassInternal implements IMessageServ return result.insertedId; } + + async beforeSave({ + message, + room: _room, + user: _user, + }: { + message: IMessage; + room: IRoom; + user: Pick; + }): Promise { + // TODO looks like this one was not being used (so I'll left it commented) + // await this.joinDiscussionOnMessage({ message, room, user }); + + // conditionals here should be fast, so they won't add up for each message + if (this.isBadWordsFilterEnabled()) { + message = await this.filterBadWords(message); + } + + return message; + } + + private isBadWordsFilterEnabled() { + return !!settings.get('Message_AllowBadWordsFilter'); + } + + private async filterBadWords(message: IMessage): Promise { + if (!message.msg || !this.badWordsFilter) { + return message; + } + + try { + message.msg = this.badWordsFilter.clean(message.msg); + } catch (error) { + // ignore + } + + return message; + } + + // joinDiscussionOnMessage + // private async joinDiscussionOnMessage({ message, room, user }: { message: IMessage; room: IRoom; user: IUser }) { + // // abort if room is not a discussion + // if (!room.prid) { + // return; + // } + + // // check if user already joined the discussion + // const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, message.u._id, { + // projection: { _id: 1 }, + // }); + + // if (sub) { + // return; + // } + + // await Room.join({ room, user }); + // } } diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index d5c17f545fbfe..3e492a4d6514d 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -32,10 +32,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // handle agent disconnections this.onEvent('watch.pbxevents', async ({ data }) => { - this.logger.debug(`Get event watch.pbxevents on service`); const extension = data.agentExtension; if (!extension) { - this.logger.debug(`No agent extension associated with the event. Skipping`); return; } switch (data.event) { @@ -53,12 +51,12 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn this.logger.info(`Processing hangup event for call with agent on extension ${extension}`); const agent = await Users.findOneByExtension(extension); if (!agent) { - this.logger.debug(`No agent found with extension ${extension}. Event won't proceed`); + this.logger.error(`No agent found with extension ${extension}. Event won't proceed`); return; } const currentRoom = await VoipRoom.findOneByAgentId(agent._id); if (!currentRoom) { - this.logger.debug(`No active call found for agent ${agent._id}`); + this.logger.error(`No active call found for agent ${agent._id}`); return; } this.logger.debug(`Notifying agent ${agent._id} of hangup on room ${currentRoom._id}`); @@ -69,7 +67,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn this.logger.info(`Processing disconnection event for agent with extension ${extension}`); const agent = await Users.findOneByExtension(extension); if (!agent) { - this.logger.debug(`No agent found with extension ${extension}. Event won't proceed`); + this.logger.error(`No agent found with extension ${extension}. Event won't proceed`); // this should not even be possible, but just in case return; } @@ -96,8 +94,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn const { _id, department: departmentId } = guest; const newRoomAt = new Date(); - this.logger.debug(`Creating Voip room for visitor ${_id}`); - /** * This is a peculiar case for outbound. In case of outbound, * the room is created as soon as the remote use accepts a call. @@ -173,7 +169,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn uids: [], autoTranslateLanguage: '', - responseBy: '', livechatData: '', u: { _id: agent.agentId, @@ -183,7 +178,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn _updatedAt: newRoomAt, }; - this.logger.debug(`Room created for visitor ${_id}`); return (await VoipRoom.insertOne(room)).insertedId; } @@ -235,11 +229,9 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn direction: IVoipRoom['direction'], options: FindOptions = {}, ): Promise { - this.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`); let room = await VoipRoom.findOneById(rid, options); let newRoom = false; if (room && !room.open) { - this.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`); room = null; } if (room == null) { @@ -247,10 +239,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn const roomId = await this.createVoipRoom(rid, name, agent, guest, direction); room = await VoipRoom.findOneVoipRoomById(roomId); newRoom = true; - this.logger.debug(`Room obtained for visitor ${guest._id} -> ${room?._id}`); } if (!room) { - this.logger.debug(`Visitor ${guest._id} trying to access another visitor's room`); throw new Error('cannot-access-room'); } return { @@ -282,7 +272,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly' = 'voip-call-wrapup', options?: { comment?: string; tags?: string[] }, ): Promise { - this.logger.debug(`Attempting to close room ${room._id}`); if (!room || room.t !== 'v' || !room.open) { return false; } @@ -299,8 +288,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // For now, this data will be appended as a metric on room closing await this.setCallWaitingQueueTimers(room); - this.logger.debug(`Room ${room._id} closed and timers set`); - this.logger.debug(`Room ${room._id} was closed at ${closeInfo.closedAt} (duration ${closeInfo.callDuration})`); await VoipRoom.closeByRoomId(room._id, closeInfo); return true; @@ -453,8 +440,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn }, }; - this.logger.debug(`Handling event ${event} on room ${room._id}`); - if ( isVoipRoom(room) && room.open && @@ -462,7 +447,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // Check if call exists by looking if we have pbx events of it (await PbxEvents.findOneByUniqueId(room.callUniqueId)) ) { - this.logger.debug(`Room is valid. Sending event ${event}`); await sendMessage(user, message, room); } else { this.logger.warn({ msg: 'Invalid room type or event type', type: room.t, event }); diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 8bf5453587ce9..cbedf1cdcdec4 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -15,29 +15,28 @@ export class OmnichannelQueue implements IOmnichannelQueue { private queues: (string | undefined)[] = []; private delay() { - const timeout = settings.get('Omnichannel_queue_delay_timeout'); + const timeout = settings.get('Omnichannel_queue_delay_timeout') ?? 5; return timeout < 1 ? DEFAULT_RACE_TIMEOUT : timeout * 1000; } async start() { - queueLogger.debug('Starting queue'); if (this.running) { - queueLogger.debug('Queue already running'); return; } const activeQueues = await this.getActiveQueues(); queueLogger.debug(`Active queues: ${activeQueues.length}`); - this.running = true; + + queueLogger.info('Service started'); return this.execute(); } async stop() { - queueLogger.debug('Stopping queue'); await LivechatInquiry.unlockAll(); this.running = false; + queueLogger.info('Service stopped'); } private async getActiveQueues() { @@ -62,7 +61,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { const queue = await this.nextQueue(); const queueDelayTimeout = this.delay(); - queueLogger.debug(`Executing queue ${queue || 'Public'} with timeout of ${queueDelayTimeout}`); + queueLogger.info(`Executing queue ${queue || 'Public'} with timeout of ${queueDelayTimeout}`); setTimeout(this.checkQueue.bind(this, queue), queueDelayTimeout); } diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 7f35de104e1cd..61c22505ca98d 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -2,7 +2,7 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IOmnichannelService } from '@rocket.chat/core-services'; import type { IOmnichannelQueue } from '@rocket.chat/core-typings'; -import { Livechat } from '../../../app/livechat/server'; +import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../app/settings/server'; import { OmnichannelQueue } from './queue'; diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index ac978da88c778..7b9b85cecbd01 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -1,6 +1,6 @@ -import { ServiceClassInternal, Authorization } from '@rocket.chat/core-services'; +import { ServiceClassInternal, Authorization, MeteorError } from '@rocket.chat/core-services'; import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services'; -import type { AtLeast, IRoom, IUser } from '@rocket.chat/core-typings'; +import { type AtLeast, type IRoom, type IUser, isRoomWithJoinCode } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { saveRoomTopic } from '../../../app/channel-settings/server/functions/saveRoomTopic'; @@ -8,6 +8,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom'; import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName'; +import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { createDirectMessage } from '../../methods/createDirectMessage'; @@ -22,15 +23,13 @@ export class RoomService extends ServiceClassInternal implements IRoomService { throw new Error('no-permission'); } - const user = await Users.findOneById>(uid, { - projection: { username: 1 }, - }); + const user = await Users.findOneById(uid); if (!user?.username) { throw new Error('User not found'); } // TODO convert `createRoom` function to "raw" and move to here - return createRoom(type, name, user.username, members, false, readOnly, extraData, options) as unknown as IRoom; + return createRoom(type, name, user, members, false, readOnly, extraData, options) as unknown as IRoom; } async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> { @@ -94,4 +93,29 @@ export class RoomService extends ServiceClassInternal implements IRoomService { async getRouteLink(room: AtLeast): Promise { return roomCoordinator.getRouteLink(room.t as string, { rid: room._id, name: room.name }); } + + /** + * Method called by users to join a room. + */ + async join({ room, user, joinCode }: { room: IRoom; user: Pick; joinCode?: string }) { + if (!(await roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.JOIN, user._id))) { + throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); + } + + if (!(await Authorization.canAccessRoom(room, user))) { + throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); + } + + if ( + isRoomWithJoinCode(room) && + (!joinCode || joinCode !== room.joinCode) && + !(await Authorization.hasPermission(user._id, 'join-without-join-code')) + ) { + throw new MeteorError('error-code-invalid', 'Invalid Room Password', { + method: 'joinRoom', + }); + } + + return addUserToRoom(room._id, user); + } } diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 77cdc1cbd8e00..c9079b0a2bfbe 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -618,6 +618,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf caller: call.createdBy, avatar: getUserAvatarURL(call.createdBy.username), status: call.status, + callId: call._id, }, userId: calleeId, notId: PushNotification.getNotificationId(`${call.rid}|${call._id}`), diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts index a7592829578dd..ccc87b0ffd241 100644 --- a/apps/meteor/server/settings/accounts.ts +++ b/apps/meteor/server/settings/accounts.ts @@ -745,50 +745,60 @@ export const createAccountSettings = () => await this.section('Password_Policy', async function () { await this.add('Accounts_Password_Policy_Enabled', false, { type: 'boolean', + public: true, }); const enableQuery = { _id: 'Accounts_Password_Policy_Enabled', value: true, + public: true, }; await this.add('Accounts_Password_Policy_MinLength', 7, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_MaxLength', -1, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_ForbidRepeatingCharacters', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneLowercase', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneUppercase', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneNumber', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true, { type: 'boolean', + public: true, enableQuery, }); }); diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index b0cda60fe60a9..17dd1f7b230df 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -245,6 +245,14 @@ export const createMessageSettings = () => public: true, }); + await this.add('AutoTranslate_AutoEnableOnJoinRoom', false, { + type: 'boolean', + group: 'Message', + section: 'AutoTranslate', + public: true, + enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }], + }); + await this.add('AutoTranslate_ServiceProvider', 'google-translate', { type: 'select', group: 'Message', diff --git a/apps/meteor/server/settings/misc.ts b/apps/meteor/server/settings/misc.ts index 127d0e6e97bae..fa7b6bbde3d0e 100644 --- a/apps/meteor/server/settings/misc.ts +++ b/apps/meteor/server/settings/misc.ts @@ -1,11 +1,70 @@ -import { Random } from '@rocket.chat/random'; +import crypto from 'crypto'; -import { settingsRegistry } from '../../app/settings/server'; +import { Logger } from '@rocket.chat/logger'; +import { Settings } from '@rocket.chat/models'; +import { v4 as uuidv4 } from 'uuid'; + +import { settingsRegistry, settings } from '../../app/settings/server'; + +const logger = new Logger('FingerPrint'); + +const generateFingerprint = function () { + const siteUrl = settings.get('Site_Url'); + const dbConnectionString = process.env.MONGO_URL; + + const fingerprint = `${siteUrl}${dbConnectionString}`; + return crypto.createHash('sha256').update(fingerprint).digest('base64'); +}; + +const updateFingerprint = async function (fingerprint: string, verified: boolean) { + await Settings.updateValueById('Deployment_FingerPrint_Hash', fingerprint); + + await Settings.updateValueById('Deployment_FingerPrint_Verified', verified); +}; + +const verifyFingerPrint = async function () { + const DeploymentFingerPrintRecordHash = await Settings.getValueById('Deployment_FingerPrint_Hash'); + + const fingerprint = generateFingerprint(); + + if (!DeploymentFingerPrintRecordHash) { + logger.info('Generating fingerprint for the first time', fingerprint); + await updateFingerprint(fingerprint, true); + return; + } + + if (DeploymentFingerPrintRecordHash === fingerprint) { + return; + } + + if (process.env.AUTO_ACCEPT_FINGERPRINT === 'true') { + logger.info('Updating fingerprint as AUTO_ACCEPT_FINGERPRINT is true', fingerprint); + await updateFingerprint(fingerprint, true); + } + + logger.warn('Updating fingerprint as pending for admin verification', fingerprint); + await updateFingerprint(fingerprint, false); +}; + +settings.watch('Site_Url', () => { + void verifyFingerPrint(); +}); // Insert server unique id if it doesn't exist export const createMiscSettings = async () => { - await settingsRegistry.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), { + await settingsRegistry.add('uniqueID', process.env.DEPLOYMENT_ID || uuidv4(), { + public: true, + }); + + await settingsRegistry.add('Deployment_FingerPrint_Hash', '', { + public: false, + readonly: true, + }); + + await settingsRegistry.add('Deployment_FingerPrint_Verified', false, { + type: 'boolean', public: true, + readonly: true, }); await settingsRegistry.add('Initial_Channel_Created', false, { diff --git a/apps/meteor/server/settings/omnichannel.ts b/apps/meteor/server/settings/omnichannel.ts index 70fa92d347b8f..cc9da54748627 100644 --- a/apps/meteor/server/settings/omnichannel.ts +++ b/apps/meteor/server/settings/omnichannel.ts @@ -534,6 +534,16 @@ export const createOmniSettings = () => enableQuery: [{ _id: 'Livechat_Routing_Method', value: 'External' }, omnichannelEnabledQuery], }); + await this.add('Omnichannel_queue_delay_timeout', 5, { + type: 'int', + group: 'Omnichannel', + section: 'Queue_management', + i18nLabel: 'Queue_delay_timeout', + i18nDescription: 'Time_in_seconds', + enableQuery: [{ _id: 'Livechat_Routing_Method', value: { $ne: 'Manual_Selection' } }, omnichannelEnabledQuery], + invalidValue: 5, + }); + await this.add('Livechat_Allow_collect_and_store_HTTP_header_informations', false, { type: 'boolean', group: 'Omnichannel', @@ -768,7 +778,7 @@ await settingsRegistry.addGroup('SMS', async function () { i18nLabel: 'Mobex_sms_gateway_password', }); await this.add('SMS_Mobex_from_number', '', { - type: 'int', + type: 'string', enableQuery: { _id: 'SMS_Service', value: 'mobex', diff --git a/apps/meteor/server/settings/setup-wizard.ts b/apps/meteor/server/settings/setup-wizard.ts index b5cdb4f6a4b1e..9799c2017afd7 100644 --- a/apps/meteor/server/settings/setup-wizard.ts +++ b/apps/meteor/server/settings/setup-wizard.ts @@ -1204,6 +1204,13 @@ export const createSetupWSettings = () => secret: true, }); + await this.add('Cloud_Workspace_Supported_Versions_Token', '', { + type: 'string', + hidden: true, + readonly: true, + secret: true, + }); + await this.add('Cloud_Url', 'https://cloud.rocket.chat', { type: 'string', hidden: true, @@ -1263,7 +1270,7 @@ export const createSetupWSettings = () => secret: true, }); - await this.add('Cloud_Workspace_Client_Secret_Expires_At', '', { + await this.add('Cloud_Workspace_Client_Secret_Expires_At', 0, { type: 'int', hidden: true, readonly: true, diff --git a/apps/meteor/server/settings/troubleshoot.ts b/apps/meteor/server/settings/troubleshoot.ts index ecc0d37dc7117..bfecb1a344f66 100644 --- a/apps/meteor/server/settings/troubleshoot.ts +++ b/apps/meteor/server/settings/troubleshoot.ts @@ -32,16 +32,30 @@ export const createTroubleshootSettings = () => type: 'boolean', i18nDescription: 'Troubleshoot_Disable_Livechat_Activity_Monitor_Alert', }); - await this.add('Troubleshoot_Disable_Statistics_Generator', false, { - type: 'boolean', - i18nDescription: 'Troubleshoot_Disable_Statistics_Generator_Alert', - }); + await this.add('Troubleshoot_Disable_Data_Exporter_Processor', false, { type: 'boolean', i18nDescription: 'Troubleshoot_Disable_Data_Exporter_Processor_Alert', }); + await this.add('Troubleshoot_Disable_Teams_Mention', false, { + type: 'boolean', + i18nDescription: 'Troubleshoot_Disable_Teams_Mention_Alert', + }); + + // TODO: remove this setting at next major (7.0.0) + await this.add('Troubleshoot_Disable_Statistics_Generator', false, { + type: 'boolean', + i18nDescription: 'Troubleshoot_Disable_Statistics_Generator_Alert', + private: true, + hidden: true, + readonly: true, + }); + // TODO: remove this setting at next major (7.0.0) await this.add('Troubleshoot_Disable_Workspace_Sync', false, { type: 'boolean', i18nDescription: 'Troubleshoot_Disable_Workspace_Sync_Alert', + private: true, + hidden: true, + readonly: true, }); }); diff --git a/apps/meteor/server/startup/migrations/v278.ts b/apps/meteor/server/startup/migrations/v278.ts index 57986fd1064f8..068d86499ff98 100644 --- a/apps/meteor/server/startup/migrations/v278.ts +++ b/apps/meteor/server/startup/migrations/v278.ts @@ -1,7 +1,7 @@ +import { License } from '@rocket.chat/license'; import { Banners, Settings } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; -import { isEnterprise } from '../../../ee/app/license/server'; import { addMigration } from '../../lib/migrations'; addMigration({ @@ -16,7 +16,7 @@ addMigration({ const LDAPEnabled = settings.get('LDAP_Enable'); const SAMLEnabled = settings.get('SAML_Custom_Default'); - const isEE = isEnterprise(); + const isEE = License.hasValidLicense(); if (!isEE && (isCustomOAuthEnabled || LDAPEnabled || SAMLEnabled)) { return; diff --git a/apps/meteor/server/startup/migrations/xrun.js b/apps/meteor/server/startup/migrations/xrun.js index bd3d19a7cbeeb..1af7cb8ad8ad2 100644 --- a/apps/meteor/server/startup/migrations/xrun.js +++ b/apps/meteor/server/startup/migrations/xrun.js @@ -1,9 +1,11 @@ import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; -import { migrateDatabase, onFreshInstall } from '../../lib/migrations'; +import { migrateDatabase, onServerVersionChange } from '../../lib/migrations'; const { MIGRATION_VERSION = 'latest' } = process.env; const [version, ...subcommands] = MIGRATION_VERSION.split(','); await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands); -await onFreshInstall(upsertPermissions); + +// if the server is starting with a different version we update the permissions +await onServerVersionChange(() => upsertPermissions()); diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index e11324a47a466..8aba28addfcf7 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -2,9 +2,9 @@ import { faker } from '@faker-js/faker'; import { expect } from 'chai'; import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; import { api, credentials, methodCall, request } from '../api-data'; -import { IUserCredentialsHeader, password } from '../user'; -import { login } from '../users.helper'; -import { createAgent, makeAgentAvailable } from './rooms'; +import { IUserCredentialsHeader } from '../user'; +import { createAnOnlineAgent } from './users'; +import { WithRequiredProperty } from './utils'; export const NewDepartmentData = ((): Partial => ({ enabled: true, @@ -59,29 +59,19 @@ new Promise((resolve, reject) => { export const createDepartmentWithAnOnlineAgent = async (): Promise<{department: ILivechatDepartment, agent: { credentials: IUserCredentialsHeader; - user: IUser; + user: WithRequiredProperty; }}> => { - // TODO moving here for tests - const username = `user.test.${Date.now()}`; - const email = `${username}@rocket.chat`; - const { body } = await request - .post(api('users.create')) - .set(credentials) - .send({ email, name: username, username, password }); - const agent = body.user; - const createdUserCredentials = await login(agent.username, password); - await createAgent(agent.username); - await makeAgentAvailable(createdUserCredentials); + const { user, credentials } = await createAnOnlineAgent(); const department = await createDepartmentWithMethod() as ILivechatDepartment; - await addOrRemoveAgentFromDepartment(department._id, {agentId: agent._id, username: (agent.username as string)}, true); + await addOrRemoveAgentFromDepartment(department._id, {agentId: user._id, username: user.username}, true); return { department, agent: { - credentials: createdUserCredentials, - user: agent, + credentials, + user, } }; }; diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index c2658c73af8db..5efb279dcb184 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -185,6 +185,9 @@ export const getLivechatRoomInfo = (roomId: string): Promise = }); }; +/** + * @summary Sends message as visitor +*/ export const sendMessage = (roomId: string, message: string, visitorToken: string): Promise => { return new Promise((resolve, reject) => { request diff --git a/apps/meteor/tests/data/livechat/users.ts b/apps/meteor/tests/data/livechat/users.ts index 7a5dc23b4cc02..38fb176faaa48 100644 --- a/apps/meteor/tests/data/livechat/users.ts +++ b/apps/meteor/tests/data/livechat/users.ts @@ -1,6 +1,6 @@ import { faker } from "@faker-js/faker"; import type { IUser } from "@rocket.chat/core-typings"; -import { password } from "../user"; +import { IUserCredentialsHeader, password } from "../user"; import { createUser, login } from "../users.helper"; import { createAgent, makeAgentAvailable } from "./rooms"; import { api, credentials, request } from "../api-data"; @@ -29,3 +29,24 @@ export const removeAgent = async (userId: string): Promise => { .set(credentials) .expect(200); } + +export const createAnOnlineAgent = async (): Promise<{ + credentials: IUserCredentialsHeader; + user: IUser & { username: string }; +}> => { + const username = `user.test.${Date.now()}`; + const email = `${username}@rocket.chat`; + const { body } = await request + .post(api('users.create')) + .set(credentials) + .send({ email, name: username, username, password }); + const agent = body.user; + const createdUserCredentials = await login(agent.username, password); + await createAgent(agent.username); + await makeAgentAvailable(createdUserCredentials); + + return { + credentials: createdUserCredentials, + user: agent, + }; +} diff --git a/apps/meteor/tests/data/livechat/utils.ts b/apps/meteor/tests/data/livechat/utils.ts index 89b6af709fbf0..b6fd3a4bf6b36 100644 --- a/apps/meteor/tests/data/livechat/utils.ts +++ b/apps/meteor/tests/data/livechat/utils.ts @@ -1,6 +1,10 @@ export type DummyResponse = E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T }; +export type WithRequiredProperty = Type & { + [Property in Key]-?: Type[Property]; +}; + export const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/meteor/tests/data/users.helper.js b/apps/meteor/tests/data/users.helper.js index 9dac1772dcdc1..92425902cb5b0 100644 --- a/apps/meteor/tests/data/users.helper.js +++ b/apps/meteor/tests/data/users.helper.js @@ -77,3 +77,15 @@ export const getMe = (overrideCredential = credentials) => resolve(res.body); }); }); + +export const setUserActiveStatus = (userId, activeStatus = true) => + new Promise((resolve) => { + request + .post(api('users.setActiveStatus')) + .set(credentials) + .send({ + userId, + activeStatus, + }) + .end(resolve); + }); diff --git a/apps/meteor/tests/e2e/account-profile.spec.ts b/apps/meteor/tests/e2e/account-profile.spec.ts index b850b7855c6f8..49d96772bf2d2 100644 --- a/apps/meteor/tests/e2e/account-profile.spec.ts +++ b/apps/meteor/tests/e2e/account-profile.spec.ts @@ -64,6 +64,15 @@ test.describe.serial('settings-account-profile', () => { }); }); + test.describe('Security', () => { + test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => { + await page.goto('/account/security'); + + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) + }) + test('Personal Access Tokens', async ({ page }) => { const response = page.waitForResponse('**/api/v1/users.getPersonalAccessTokens'); await page.goto('/account/tokens'); diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 0c49ea7c7bbf3..7cc04efba3a04 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -154,8 +154,7 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); - await page.mouse.move(0, 0); + await poHomeChannel.dismissToast(); await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); @@ -168,6 +167,7 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.tabs.btnDisableE2E).toBeVisible(); await poHomeChannel.tabs.btnDisableE2E.click({ force: true }); + await poHomeChannel.dismissToast(); await page.waitForTimeout(1000); await poHomeChannel.content.sendMessage('hello world not encrypted'); @@ -178,6 +178,7 @@ test.describe.serial('e2e-encryption', () => { await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); + await poHomeChannel.dismissToast(); await page.waitForTimeout(1000); await poHomeChannel.content.sendMessage('hello world encrypted again'); @@ -197,7 +198,7 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); - await poHomeChannel.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); + await poHomeChannel.dismissToast(); await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); diff --git a/apps/meteor/tests/e2e/forgot-password.spec.ts b/apps/meteor/tests/e2e/forgot-password.spec.ts index ee531d59cdfa9..441944f5b2272 100644 --- a/apps/meteor/tests/e2e/forgot-password.spec.ts +++ b/apps/meteor/tests/e2e/forgot-password.spec.ts @@ -2,7 +2,7 @@ import { Registration } from './page-objects'; import { test, expect } from './utils/test'; test.describe.parallel('Forgot Password', () => { - let poRegistration: Registration; + let poRegistration: Registration; test.beforeEach(async ({ page }) => { poRegistration = new Registration(page); @@ -11,7 +11,7 @@ test.describe.parallel('Forgot Password', () => { await poRegistration.btnForgotPassword.click(); }); - test('Email validation', async () => { + test('Send email to recover account', async () => { await test.step('expect trigger a validation error if no email is provided', async () => { await poRegistration.btnSendInstructions.click(); await expect(poRegistration.inputEmail).toBeInvalid(); @@ -31,11 +31,16 @@ test.describe.parallel('Forgot Password', () => { await expect(poRegistration.inputEmail).toBeInvalid(); }); - await test.step('expect to show a success toast if a valid email is provided', async () => { + await test.step('expect to show a success callout if a valid email is provided', async () => { await poRegistration.inputEmail.fill('mail@mail.com'); await poRegistration.btnSendInstructions.click(); await expect(poRegistration.forgotPasswordEmailCallout).toBeVisible(); }); }); + + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) }); diff --git a/apps/meteor/tests/e2e/homepage.spec.ts b/apps/meteor/tests/e2e/homepage.spec.ts index 380fa54d2af3e..4f8f9d09a2f3a 100644 --- a/apps/meteor/tests/e2e/homepage.spec.ts +++ b/apps/meteor/tests/e2e/homepage.spec.ts @@ -32,7 +32,7 @@ test.describe.serial('homepage', () => { await adminPage.close(); }); - test('layout', async () => { + test('expect customize button and all cards to be visible', async () => { await test.step('expect show customize button', async () => { await expect(adminPage.locator('role=button[name="Customize"]')).toBeVisible(); }); @@ -47,7 +47,7 @@ test.describe.serial('homepage', () => { await expect((await api.post('/settings/Layout_Home_Body', { value: '' })).status()).toBe(200); }); - test('layout', async () => { + test('visibility and button functionality in custom body with empty custom content', async () => { await test.step('expect default value in custom body', async () => { await expect( adminPage.locator('role=status[name="Admins may insert content html to be rendered in this white space."]'), @@ -70,7 +70,7 @@ test.describe.serial('homepage', () => { await expect((await api.post('/settings/Layout_Home_Body', { value: 'Hello admin' })).status()).toBe(200); }); - test('layout', async () => { + test('visibility and button functionality in custom body with custom content', async () => { await test.step('expect custom body to be visible', async () => { await expect(adminPage.locator('role=status[name="Hello admin"]')).toBeVisible(); }); @@ -122,7 +122,7 @@ test.describe.serial('homepage', () => { await regularUserPage.close(); }); - test('layout', async () => { + test('the option customize is not be active', async () => { await test.step('expect to not show customize button', async () => { await expect(regularUserPage.locator('role=button[name="Customize"]')).not.toBeVisible(); }); @@ -162,7 +162,7 @@ test.describe.serial('homepage', () => { expect((await api.post('/settings/Layout_Home_Title', { value: 'Home' })).status()).toBe(200); }); - test('layout', async () => { + test('expect welcome text and header text to be correct', async () => { await test.step('expect welcome text to be NewSiteName', async () => { await expect(regularUserPage.locator('role=heading[name="Welcome to NewSiteName"]')).toBeVisible(); }); @@ -202,7 +202,7 @@ test.describe.serial('homepage', () => { expect((await api.post('/settings/Layout_Custom_Body_Only', { value: false })).status()).toBe(200); }); - test('layout', async () => { + test('expect default layout not be visible and custom body visible', async () => { await test.step('expect default layout to not be visible', async () => { await expect(regularUserPage.locator('[data-qa-id="homepage-welcome-text"]')).not.toBeVisible(); }); diff --git a/apps/meteor/tests/e2e/login.spec.ts b/apps/meteor/tests/e2e/login.spec.ts index 2414e5579e3b5..958f5120f1422 100644 --- a/apps/meteor/tests/e2e/login.spec.ts +++ b/apps/meteor/tests/e2e/login.spec.ts @@ -12,6 +12,11 @@ test.describe.parallel('Login', () => { await page.goto('/home'); }); + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) + test('Login with invalid credentials', async () => { await test.step('expect to have username and password marked as invalid', async () => { await poRegistration.username.type(faker.internet.email()); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index f6093053fde60..7cfa089326b2d 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -40,6 +40,8 @@ test.describe.serial('message-actions', () => { await page.locator('[data-qa-id="edit-message"]').click(); await page.locator('[name="msg"]').fill('this message was edited'); await page.keyboard.press('Enter'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this message was edited'); }); test('expect message is deleted', async ({ page }) => { @@ -47,6 +49,9 @@ test.describe.serial('message-actions', () => { await poHomeChannel.content.openLastMessageMenu(); await page.locator('[data-qa-id="delete-message"]').click(); await page.locator('#modal-root .rcx-button-group--align-end .rcx-button--danger').click(); + await expect(poHomeChannel.content.lastUserMessage.locator('[data-qa-type="message-body"]:has-text("Message to delete")')).toHaveCount( + 0, + ); }); test('expect quote the message', async ({ page }) => { @@ -64,6 +69,9 @@ test.describe.serial('message-actions', () => { await poHomeChannel.content.sendMessage('Message to star'); await poHomeChannel.content.openLastMessageMenu(); await page.locator('[data-qa-id="star-message"]').click(); + await page.getByRole('button').and(page.getByTitle('Options')).click(); + await page.locator('[data-key="starred-messages"]').click(); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('Message to star'); }); test('expect copy the message', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts new file mode 100644 index 0000000000000..323d415505925 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts @@ -0,0 +1,89 @@ +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; + +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { test, expect } from '../utils/test'; + +test.skip(!IS_EE, 'Export transcript as PDF > Enterprie Only'); + +test.describe('omnichannel- export chat transcript as PDF', () => { + let poLiveChat: OmnichannelLiveChat; + let newUser: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeOmnichannel }; + test.beforeAll(async ({ api, browser }) => { + newUser = { + name: faker.person.firstName(), + email: faker.internet.email(), + }; + + // Set user user 1 as manager and agent + await api.post('/livechat/users/agent', { username: 'user1' }); + await api.post('/livechat/users/manager', { username: 'user1' }); + + const { page } = await createAuxContext(browser, Users.user1); + agent = { page, poHomeChannel: new HomeOmnichannel(page) }; + }); + test.beforeEach(async ({ page, api }) => { + poLiveChat = new OmnichannelLiveChat(page, api); + }); + + test.afterAll(async ({ api }) => { + await api.delete('/livechat/users/agent/user1'); + await api.delete('/livechat/users/manager/user1'); + await agent.page.close(); + }); + + test('Export PDF transcript', async ({ page }) => { + await test.step('Expect send a message as a visitor', async () => { + await page.goto('/livechat'); + await poLiveChat.openLiveChat(); + await poLiveChat.sendMessage(newUser, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { + await new Promise((resolve) => setTimeout(resolve, 5000)); + await agent.poHomeChannel.sidenav.openChat(newUser.name); + }); + + await test.step('Expect to be not able send transcript as PDF', async () => { + await agent.poHomeChannel.content.btnSendTranscript.click(); + await agent.poHomeChannel.content.btnSendTranscriptAsPDF.hover(); + await expect(agent.poHomeChannel.content.btnSendTranscriptAsPDF).toHaveAttribute('aria-disabled', 'true'); + }); + + await test.step('Expect chat to be closed', async () => { + await agent.poHomeChannel.content.btnCloseChat.click(); + await agent.poHomeChannel.content.inputModalClosingComment.type('any_comment'); + await agent.poHomeChannel.transcript.checkboxPDF.click(); + await agent.poHomeChannel.content.btnModalConfirm.click(); + await expect(agent.poHomeChannel.toastSuccess).toBeVisible(); + }); + + // Exported PDF can be downloaded from rocket.cat room + await test.step('Expect to have exported PDF in rocket.cat', async () => { + await page.waitForTimeout(3000); + await agent.poHomeChannel.sidenav.openChat('rocket.cat'); + await expect(agent.poHomeChannel.transcript.DownloadedPDF).toBeVisible(); + }); + + // PDF can be exported from Omnichannel Contact Center + await test.step('Expect to have exported PDF in rocket.cat', async () => { + await agent.poHomeChannel.transcript.contactCenter.click(); + await agent.poHomeChannel.transcript.contactCenterChats.click(); + await agent.poHomeChannel.transcript.contactCenterSearch.type(newUser.name); + await page.waitForTimeout(3000); + await agent.poHomeChannel.transcript.firstRow.click(); + await agent.poHomeChannel.transcript.viewFullConversation.click(); + await agent.poHomeChannel.content.btnSendTranscript.click(); + await expect(agent.poHomeChannel.content.btnSendTranscriptAsPDF).toHaveAttribute('aria-disabled', 'false'); + await agent.poHomeChannel.content.btnSendTranscriptAsPDF.click(); + await expect(agent.poHomeChannel.toastSuccess).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts index 4cf3b82b2c66d..9db221723ebee 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts @@ -29,6 +29,7 @@ test.describe.serial('Omnichannel Triggers', () => { const { page } = await createAuxContext(browser, Users.user1, '/omnichannel/triggers'); agent = { page, poHomeOmnichannel: new HomeOmnichannel(page) }; + await page.emulateMedia({ reducedMotion: 'reduce' }); }); test.beforeEach(async ({ page, api }) => { @@ -39,6 +40,7 @@ test.describe.serial('Omnichannel Triggers', () => { await Promise.all([ api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/manager/user1'), + api.delete(`/livechat/triggers/${triggersName}`), api.post('/settings/Livechat_clear_local_storage_when_chat_ended', { value: false }), ]); await agent.page.close(); diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts index 26537f1831135..9b47d2e44adca 100644 --- a/apps/meteor/tests/e2e/page-objects/auth.ts +++ b/apps/meteor/tests/e2e/page-objects/auth.ts @@ -11,6 +11,11 @@ export class Registration { return this.page.locator('role=button[name="Send instructions"]'); } + get btnReset(): Locator { + return this.page.locator('role=button[name="Reset"]'); + } + + get btnLogin(): Locator { return this.page.locator('role=button[name="Login"]'); } @@ -19,10 +24,6 @@ export class Registration { return this.page.locator('role=link[name="Create an account"]'); } - get main(): Locator { - return this.page.locator('role=main'); - } - get backToLogin(): Locator { return this.page.locator('role=link[name="Back to Login"]'); } @@ -63,10 +64,6 @@ export class Registration { return this.page.locator('[name=passwordConfirmation]'); } - // get textErrorPasswordConfirm(): Locator { - // return this.page.locator('[name=confirm-pass]~.input-error'); - // } - get forgotPasswordEmailCallout(): Locator { return this.page.locator('role=status'); } diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index d2f43c5cfec51..24403b22b8451 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -34,4 +34,10 @@ export class HomeChannel { await expect(this.page.locator('role=main >> .rcx-skeleton')).toHaveCount(0); await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); } + + async dismissToast() { + // this is a workaround for when the toast is blocking the click of the button + await this.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); + await this.page.mouse.move(0, 0); + } } diff --git a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts index ce7f33244570d..983d2392a47d7 100644 --- a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts @@ -2,6 +2,7 @@ import type { Locator, Page } from '@playwright/test'; import { HomeOmnichannelContent, HomeSidenav, HomeFlextab, OmnichannelSidenav } from './fragments'; import { OmnichannelCurrentChats } from './omnichannel-current-chats'; +import { OmnichannelTranscript } from './omnichannel-transcript'; import { OmnichannelTriggers } from './omnichannel-triggers'; export class HomeOmnichannel { @@ -19,6 +20,8 @@ export class HomeOmnichannel { readonly currentChats: OmnichannelCurrentChats; + readonly transcript: OmnichannelTranscript; + constructor(page: Page) { this.page = page; this.content = new HomeOmnichannelContent(page); @@ -27,6 +30,7 @@ export class HomeOmnichannel { this.triggers = new OmnichannelTriggers(page); this.omnisidenav = new OmnichannelSidenav(page); this.currentChats = new OmnichannelCurrentChats(page); + this.transcript = new OmnichannelTranscript(page); } get toastSuccess(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/home-team.ts b/apps/meteor/tests/e2e/page-objects/home-team.ts index 24e8396979c9a..9c890da05db8c 100644 --- a/apps/meteor/tests/e2e/page-objects/home-team.ts +++ b/apps/meteor/tests/e2e/page-objects/home-team.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab } from './fragments'; +import { HomeContent, HomeFlextab, HomeSidenav } from './fragments'; export class HomeTeam { private readonly page: Page; @@ -30,4 +30,12 @@ export class HomeTeam { get btnTeamCreate(): Locator { return this.page.locator('role=dialog >> role=group >> role=button[name=Create]'); } + + get textPrivate(): Locator { + return this.page.locator('role=dialog[name="Create Team"] >> label >> text="Private"'); + } + + get textReadOnly(): Locator { + return this.page.locator('role=dialog[name="Create Team"] >> label >> text="Read Only"'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts new file mode 100644 index 0000000000000..d0249933d4385 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts @@ -0,0 +1,46 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelSidenav } from './fragments'; + +export class OmnichannelTranscript { + private readonly page: Page; + + readonly sidenav: OmnichannelSidenav; + + constructor(page: Page) { + this.page = page; + this.sidenav = new OmnichannelSidenav(page); + } + + get checkboxPDF(): Locator { + return this.page.locator('//input[@name="transcriptPDF"]//following::i[1]'); + } + + get exportedPDF(): Locator { + return this.page.locator('//div[contains(text(),"PDF Transcript successfully generated")]'); + } + + get contactCenter(): Locator { + return this.page.locator('//button[@data-tooltip="Contact Center"]'); + } + + get contactCenterChats(): Locator { + return this.page.locator('//button[contains(.,"Chats")]'); + } + + get contactCenterSearch(): Locator { + return this.page.locator('[placeholder="Search"]'); + } + + get firstRow(): Locator { + return this.page.locator('//tr[1]//td[1]'); + } + + get viewFullConversation(): Locator { + return this.page.locator('//button[@title="View full conversation"]/i'); + } + + get DownloadedPDF(): Locator { + return this.page.locator('[data-qa-type="attachment-title-link"]').last(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/utils.ts b/apps/meteor/tests/e2e/page-objects/utils.ts index d71eaf55daab3..066c5eac153f6 100644 --- a/apps/meteor/tests/e2e/page-objects/utils.ts +++ b/apps/meteor/tests/e2e/page-objects/utils.ts @@ -7,6 +7,10 @@ export class Utils { this.page = page; } + get mainContent(): Locator { + return this.page.locator('main.main-content'); + } + get toastBar(): Locator { return this.page.locator('.rcx-toastbar'); } diff --git a/apps/meteor/tests/e2e/register.spec.ts b/apps/meteor/tests/e2e/register.spec.ts index f99f212ff7187..1709b8414e6b7 100644 --- a/apps/meteor/tests/e2e/register.spec.ts +++ b/apps/meteor/tests/e2e/register.spec.ts @@ -1,14 +1,16 @@ import { faker } from '@faker-js/faker'; -import { Registration } from './page-objects'; +import { Utils, Registration } from './page-objects'; import { test, expect } from './utils/test'; -test.describe.serial('register', () => { +test.describe.parallel('register', () => { let poRegistration: Registration; + let poUtils: Utils; test.describe('Registration default flow', async () => { test.beforeEach(async ({ page }) => { poRegistration = new Registration(page); + poUtils = new Utils(page); }); test('Successfully Registration flow', async ({ page }) => { await test.step('expect trigger a validation error if no data is provided on register', async () => { @@ -37,7 +39,7 @@ test.describe.serial('register', () => { await test.step('expect successfully register a new user', async () => { await poRegistration.inputPasswordConfirm.fill('any_password'); await poRegistration.btnRegister.click(); - await expect(poRegistration.main).toBeHidden(); + await expect(poUtils.mainContent).toBeVisible(); }); }); @@ -70,7 +72,7 @@ test.describe.serial('register', () => { await poRegistration.inputPassword.fill('any_password'); await poRegistration.btnRegister.click(); - await expect(poRegistration.main).toBeHidden(); + await expect(poUtils.mainContent).toBeVisible(); }); }); }); @@ -128,7 +130,7 @@ test.describe.serial('register', () => { await page.goto('/home'); await poRegistration.goToRegister.click(); - const results = await makeAxeBuilder().disableRules(['landmark-one-main', 'region']).analyze(); + const results = await makeAxeBuilder().analyze(); expect(results.violations).toEqual([]); }); @@ -137,6 +139,7 @@ test.describe.serial('register', () => { test.describe('Registration for secret password', async () => { test.beforeEach(async ({ api, page }) => { poRegistration = new Registration(page); + poUtils = new Utils(page); const result = await api.post('/settings/Accounts_RegistrationForm', { value: 'Secret URL' }); await api.post('/settings/Accounts_RegistrationForm_SecretURL', { value: 'secret' }); await expect(result.ok()).toBeTruthy(); @@ -173,8 +176,7 @@ test.describe.serial('register', () => { await poRegistration.inputPassword.fill('any_password'); await poRegistration.inputPasswordConfirm.fill('any_password'); await poRegistration.btnRegister.click(); - await page.waitForSelector('role=main'); - await expect(poRegistration.main).toBeVisible(); + await expect(poUtils.mainContent).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/reset-password.spec.ts b/apps/meteor/tests/e2e/reset-password.spec.ts new file mode 100644 index 0000000000000..fc5e0b7037847 --- /dev/null +++ b/apps/meteor/tests/e2e/reset-password.spec.ts @@ -0,0 +1,35 @@ +import { Registration } from './page-objects'; +import { setSettingValueById } from './utils/setSettingValueById'; +import { test, expect } from './utils/test'; + +test.describe.parallel('Reset Password', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ api, page }) => { + poRegistration = new Registration(page); + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', true); + + await page.goto('/reset-password/someToken'); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', true); + }) + + test('should confirm password be invalid', async () => { + await poRegistration.inputPassword.fill('123456'); + await poRegistration.inputPasswordConfirm.fill('123455'); + await poRegistration.btnReset.click(); + await expect(poRegistration.inputPasswordConfirm).toBeInvalid(); + }); + + test('should confirm password not be visible', async ({ api }) => { + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', false); + await expect(poRegistration.inputPasswordConfirm).not.toBeVisible(); + }) + + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) +}); diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index ee2220966d8d4..338f5c5eb0ef4 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; import { HomeTeam } from './page-objects'; import { createTargetChannel } from './utils'; -import { test, expect } from './utils/test'; +import { expect, test } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -11,6 +11,8 @@ test.describe.serial('teams-management', () => { let poHomeTeam: HomeTeam; let targetChannel: string; const targetTeam = faker.string.uuid(); + const targetTeamNonPrivate = faker.string.uuid(); + const targetTeamReadOnly = faker.string.uuid(); test.beforeAll(async ({ api }) => { targetChannel = await createTargetChannel(api); @@ -22,7 +24,7 @@ test.describe.serial('teams-management', () => { await page.goto('/home'); }); - test('expect create "targetTeam"', async ({ page }) => { + test('expect create "targetTeam" private', async ({ page }) => { await poHomeTeam.sidenav.openNewByLabel('Team'); await poHomeTeam.inputTeamName.type(targetTeam); await poHomeTeam.addMember('user1'); @@ -31,6 +33,26 @@ test.describe.serial('teams-management', () => { await expect(page).toHaveURL(`/group/${targetTeam}`); }); + test('expect create "targetTeamNonPrivate" non private', async ({ page }) => { + await poHomeTeam.sidenav.openNewByLabel('Team'); + await poHomeTeam.inputTeamName.type(targetTeamNonPrivate); + await poHomeTeam.textPrivate.click(); + await poHomeTeam.addMember('user1'); + await poHomeTeam.btnTeamCreate.click(); + + await expect(page).toHaveURL(`/channel/${targetTeamNonPrivate}`); + }); + + test('expect create "targetTeamReadOnly" readonly', async ({ page }) => { + await poHomeTeam.sidenav.openNewByLabel('Team'); + await poHomeTeam.inputTeamName.type(targetTeamReadOnly); + await poHomeTeam.textReadOnly.click(); + await poHomeTeam.addMember('user1'); + await poHomeTeam.btnTeamCreate.click(); + + await expect(page).toHaveURL(`/group/${targetTeamReadOnly}`); + }); + test('expect throw validation error if team name already exists', async () => { await poHomeTeam.sidenav.openNewByLabel('Team'); await poHomeTeam.inputTeamName.type(targetTeam); diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index 6d6b8ee6f1223..d9e181d2706bc 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -104,6 +104,7 @@ test.describe.serial('Threads', () => { await page.locator('[data-qa-id="edit-message"]').click(); await page.locator('[name="msg"]').last().fill('this message was edited'); await page.keyboard.press('Enter'); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this message was edited'); }); test('expect quote the thread message', async ({ page }) => { @@ -118,6 +119,9 @@ test.describe.serial('Threads', () => { test('expect star the thread message', async ({ page }) => { await poHomeChannel.content.openLastThreadMessageMenu(); await page.locator('[data-qa-id="star-message"]').click(); + await page.getByRole('button').and(page.getByTitle('Options')).click(); + await page.locator('[data-key="starred-messages"]').click(); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this is a message for reply'); }); test('expect copy the message', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/utils/test.ts b/apps/meteor/tests/e2e/utils/test.ts index 5ccea233d88aa..81479231fd65d 100644 --- a/apps/meteor/tests/e2e/utils/test.ts +++ b/apps/meteor/tests/e2e/utils/test.ts @@ -117,7 +117,7 @@ export const test = baseTest.extend({ makeAxeBuilder: async ({ page }, use) => { const SELECT_KNOW_ISSUES = ['aria-hidden-focus', 'nested-interactive'] - const makeAxeBuilder = () => new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).disableRules(['document-title', ...SELECT_KNOW_ISSUES]); + const makeAxeBuilder = () => new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).include('body').disableRules([...SELECT_KNOW_ISSUES]); await use(makeAxeBuilder); } }); diff --git a/apps/meteor/tests/end-to-end/api/00-autotranslate.js b/apps/meteor/tests/end-to-end/api/00-autotranslate.js index 52adb69f17c73..7695718bd01f4 100644 --- a/apps/meteor/tests/end-to-end/api/00-autotranslate.js +++ b/apps/meteor/tests/end-to-end/api/00-autotranslate.js @@ -1,9 +1,12 @@ import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, after, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { password } from '../../data/user'; +import { createUser, login } from '../../data/users.helper.js'; describe('AutoTranslate', function () { this.retries(0); @@ -68,6 +71,18 @@ describe('AutoTranslate', function () { }); }); describe('[/autotranslate.saveSettings', () => { + let testGroupId; + before(async () => { + await updateSetting('E2E_Enable', true); + await updateSetting('E2E_Enabled_Default_PrivateRooms', true); + const res = await createRoom({ type: 'p', name: `e2etest-autotranslate-${Date.now()}` }); + testGroupId = res.body.group._id; + }); + after(async () => { + await updateSetting('E2E_Enabled_Default_PrivateRooms', false); + await updateSetting('E2E_Enable', false); + await deleteRoom({ type: 'p', roomId: testGroupId }); + }); it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', (done) => { updateSetting('AutoTranslate_Enabled', false).then(() => { request @@ -220,6 +235,23 @@ describe('AutoTranslate', function () { }) .end(done); }); + it('should throw an error when E2E encryption is enabled', async () => { + await request + .post(api('autotranslate.saveSettings')) + .set(credentials) + .send({ + roomId: testGroupId, + field: 'autoTranslate', + defaultLanguage: 'en', + value: true, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-e2e-enabled'); + }); + }); it('should return success when the setting is saved correctly', (done) => { request .post(api('autotranslate.saveSettings')) @@ -314,5 +346,130 @@ describe('AutoTranslate', function () { .end(done); }); }); + describe('Autoenable setting', () => { + let userA; + let userB; + let credA; + let credB; + let channel; + + const createChannel = async (members, cred) => + (await createRoom({ type: 'c', members, name: `channel-test-${Date.now()}`, credentials: cred })).body.channel; + + const setLanguagePref = async (language, cred) => { + await request + .post(api('users.setPreferences')) + .set(cred) + .send({ data: { language } }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }; + + const getSub = async (roomId, cred) => + ( + await request + .get(api('subscriptions.getOne')) + .set(cred) + .query({ + roomId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').and.to.be.an('object'); + }) + ).body.subscription; + + before(async () => { + await updateSetting('AutoTranslate_Enabled', true); + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', true); + await updateSetting('Language', 'pt-BR'); + + channel = await createChannel(); + userA = await createUser(); + userB = await createUser(); + + credA = await login(userA.username, password); + credB = await login(userB.username, password); + + await setLanguagePref('en', credB); + }); + + after(async () => { + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', false); + await updateSetting('AutoTranslate_Enabled', false); + await updateSetting('Language', ''); + }); + + it("should do nothing if the user hasn't changed his language preference", async () => { + const sub = await getSub(channel._id, credentials); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it("should do nothing if the user changed his language preference to be the same as the server's", async () => { + await setLanguagePref('pt-BR', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it('should enable autotranslate with the correct language when creating a new room', async () => { + await setLanguagePref('en', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate for all the members added to the room upon creation', async () => { + const channel = await createChannel([userA.username, userB.username]); + const subA = await getSub(channel._id, credA); + expect(subA).to.have.property('autoTranslate'); + expect(subA).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + + const subB = await getSub(channel._id, credB); + expect(subB).to.have.property('autoTranslate'); + expect(subB).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when joining a room', async () => { + await request + .post(api('channels.join')) + .set(credA) + .send({ + roomId: channel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when added to a room', async () => { + await request + .post(api('channels.invite')) + .set(credentials) + .send({ + roomId: channel._id, + userId: userB._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credB); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js index 7525fd6ab443d..e9fec42e4b668 100644 --- a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js +++ b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js @@ -24,6 +24,7 @@ describe('miscellaneous', function () { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { + expect(res.body).to.have.property('version').and.to.be.a('string'); expect(res.body.info).to.have.property('version').and.to.be.a('string'); expect(res.body.info).to.have.property('build').and.to.be.an('object'); expect(res.body.info).to.have.property('commit').and.to.be.an('object'); diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index 130f365c96c52..d99fa68a036f8 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -23,7 +23,7 @@ import { imgURL } from '../../data/interactions.js'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; import { adminEmail, preferences, password, adminUsername } from '../../data/user'; -import { createUser, login, deleteUser, getUserStatus } from '../../data/users.helper.js'; +import { createUser, login, deleteUser, getUserStatus, getUserByUsername } from '../../data/users.helper.js'; async function createChannel(userCredentials, name) { const res = await request.post(api('channels.create')).set(userCredentials).send({ @@ -3447,6 +3447,24 @@ describe('[Users]', function () { .end(done); }); }); + it('users should retain their roles when they are deactivated', async () => { + const testUser = await createUser({ roles: ['user', 'livechat-agent'] }); + + await request + .post(api('users.setActiveStatus')) + .set(credentials) + .send({ + activeStatus: false, + userId: testUser._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const user = await getUserByUsername(testUser.username); + expect(user).to.have.property('roles'); + expect(user.roles).to.be.an('array').of.length(2); + expect(user.roles).to.include('user', 'livechat-agent'); + }); }); describe('[/users.deactivateIdle]', () => { diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index cccc3eb277386..3941df1366eb4 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -29,8 +29,8 @@ describe('[Groups]', function () { before((done) => getCredentials(done)); - before('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ @@ -46,10 +46,21 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.msgs', 0); group._id = res.body.group._id; group.name = res.body.group.name; + }); + }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomId: group._id, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(200); }); - describe('[/groups.create]', () => { + + describe('/groups.create', () => { let guestUser; let room; @@ -60,80 +71,89 @@ describe('[Groups]', function () { await deleteUser(guestUser); }); - it('should not add guest users to more rooms than defined in the license', async function () { - // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, - // ideally we should have a single CI job that adds a license and runs both CE and EE tests. - if (!process.env.IS_EE) { - this.skip(); - } - const promises = []; + describe('guest users', () => { + it('should not add guest users to more rooms than defined in the license', async function () { + // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, + // ideally we should have a single CI job that adds a license and runs both CE and EE tests. + if (!process.env.IS_EE) { + this.skip(); + } + const promises = []; + + for (let i = 0; i < maxRoomsPerGuest; i++) { + promises.push( + createRoom({ + type: 'p', + name: `channel.test.${Date.now()}-${Math.random()}`, + members: [guestUser.username], + }), + ); + } + await Promise.all(promises); - for (let i = 0; i < maxRoomsPerGuest; i++) { - promises.push( - createRoom({ - type: 'p', + await request + .post(api('groups.create')) + .set(credentials) + .send({ name: `channel.test.${Date.now()}-${Math.random()}`, members: [guestUser.username], - }), - ); - } - await Promise.all(promises); + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + room = res.body.group; + }); - request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `channel.test.${Date.now()}-${Math.random()}`, - members: [guestUser.username], - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - room = res.body.group; - }) - .then(() => { - request - .get(api('groups.members')) - .set(credentials) - .query({ - roomId: room._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('members').and.to.be.an('array'); - expect(res.body.members).to.have.lengthOf(1); - }); - }); + await request + .get(api('groups.members')) + .set(credentials) + .query({ + roomId: room._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + }); + }); }); - }); - describe('/groups.create (encrypted)', () => { - it('should create a new encrypted group', async () => { - await request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `encrypted-${apiPrivateChannelName}`, - extraData: { - encrypted: true, - }, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group.name', `encrypted-${apiPrivateChannelName}`); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', 0); - expect(res.body).to.have.nested.property('group.encrypted', true); - }); + + describe('validate E2E rooms', () => { + it('should create a new encrypted group', async () => { + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `encrypted-${apiPrivateChannelName}`, + extraData: { + encrypted: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.name', `encrypted-${apiPrivateChannelName}`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + expect(res.body).to.have.nested.property('group.encrypted', true); + }); + }); }); - it('should create the encrypted room by default', async () => { - await updateSetting('E2E_Enabled_Default_PrivateRooms', true); - try { + describe('E2E enabled by default', () => { + before(async () => { + await Promise.all([updateSetting('E2E_Enable', true), updateSetting('E2E_Enabled_Default_PrivateRooms', true)]); + }); + + after(async () => { + await Promise.all([updateSetting('E2E_Enable', false), updateSetting('E2E_Enabled_Default_PrivateRooms', false)]); + }); + + it('should create the encrypted room by default', async () => { await request .post(api('groups.create')) .set(credentials) @@ -149,30 +169,43 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.msgs', 0); expect(res.body).to.have.nested.property('group.encrypted', true); }); - } finally { - await updateSetting('E2E_Enabled_Default_PrivateRooms', false); - } + }); }); }); - describe('[/groups.info]', () => { + + describe('/groups.info', () => { let testGroup = {}; let groupMessage = {}; - it('creating new group...', (done) => { - request + + const newGroupInfoName = `info-private-channel-test-${Date.now()}`; + + before('creating new group...', async () => { + await request .post(api('groups.create')) .set(credentials) .send({ - name: apiPrivateChannelName, + name: newGroupInfoName, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { testGroup = res.body.group; + }); + }); + + after('deleting group...', async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: newGroupInfoName, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(200); }); - it('should return group basic structure', (done) => { - request + + it('should return group basic structure', async () => { + await request .get(api('groups.info')) .set(credentials) .query({ @@ -183,14 +216,14 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); + expect(res.body).to.have.nested.property('group.name', newGroupInfoName); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.msgs', 0); - }) - .end(done); + }); }); - it('sending a message...', (done) => { - request + + it('sending a message...', async () => { + await request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -204,11 +237,11 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); groupMessage = res.body.message; - }) - .end(done); + }); }); - it('REACTing with last message', (done) => { - request + + it('REACTing with last message', async () => { + await request .post(api('chat.react')) .set(credentials) .send({ @@ -219,11 +252,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('STARring last message', (done) => { - request + + it('STARring last message', async () => { + await request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -233,11 +266,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('PINning last message', (done) => { - request + + it('PINning last message', async () => { + await request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -247,11 +280,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('should return group structure with "lastMessage" object including pin, reaction and star(should be an array) infos', (done) => { - request + + it('should return group structure with "lastMessage" object including pin, reaction and star(should be an array) infos', async () => { + await request .get(api('groups.info')) .set(credentials) .query({ @@ -269,11 +302,10 @@ describe('[Groups]', function () { expect(group.lastMessage).to.have.property('pinnedAt').and.to.be.a('string'); expect(group.lastMessage).to.have.property('pinnedBy').and.to.be.an('object'); expect(group.lastMessage).to.have.property('starred').and.to.be.an('array'); - }) - .end(done); + }); }); - it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY', (done) => { - request + it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY', async () => { + await request .get(api('groups.messages')) .set(credentials) .query({ @@ -288,11 +320,11 @@ describe('[Groups]', function () { const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); - }) - .end(done); + }); }); - it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY even requested with count and offset params', (done) => { - request + + it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY even requested with count and offset params', async () => { + await request .get(api('groups.messages')) .set(credentials) .query({ @@ -309,178 +341,321 @@ describe('[Groups]', function () { const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); - }) - .end(done); + }); }); }); - it('/groups.invite', async () => { - const roomInfo = await getRoomInfo(group._id); - return request - .post(api('groups.invite')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); - }); + describe('/groups.invite', async () => { + let roomInfo = {}; - it('/groups.addModerator', (done) => { - request - .post(api('groups.addModerator')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + before(async () => { + roomInfo = await getRoomInfo(group._id); + }); + + it('should invite user to group', async () => { + await request + .post(api('groups.invite')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); + }); + }); }); - it('/groups.removeModerator', (done) => { - request - .post(api('groups.removeModerator')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.addModerator', () => { + it('should make user a moderator', (done) => { + request + .post(api('groups.addModerator')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.addOwner', (done) => { - request - .post(api('groups.addOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.removeModerator', () => { + it('should remove user from moderator', (done) => { + request + .post(api('groups.removeModerator')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.removeOwner', (done) => { - request - .post(api('groups.removeOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.addOwner', () => { + it('should add user as owner', (done) => { + request + .post(api('groups.addOwner')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.addLeader', (done) => { - request - .post(api('groups.addLeader')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.a.property('success', true); - }) - .end(done); + describe('/groups.removeOwner', () => { + it('should remove user from owner', (done) => { + request + .post(api('groups.removeOwner')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.removeLeader', (done) => { - request - .post(api('groups.removeLeader')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.addLeader', () => { + it('should add user as leader', (done) => { + request + .post(api('groups.addLeader')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }) + .end(done); + }); }); - it('/groups.kick', (done) => { - request - .post(api('groups.kick')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.removeLeader', () => { + it('should remove user from leader', (done) => { + request + .post(api('groups.removeLeader')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.invite', async () => { - const roomInfo = await getRoomInfo(group._id); + describe('/groups.kick', () => { + let testUserModerator; + let credsModerator; + let testUserOwner; + let credsOwner; + let testUserMember; + let groupTest; - return request - .post(api('groups.invite')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); - }); + const inviteUser = async (userId) => { + await request + .post(api('groups.invite')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }; + + before(async () => { + // had to do them in serie because calling them with Promise.all was failing some times + testUserModerator = await createUser(); + testUserOwner = await createUser(); + testUserMember = await createUser(); + + credsModerator = await login(testUserModerator.username, password); + credsOwner = await login(testUserOwner.username, password); + + await request + .post(api('groups.create')) + .set(credsOwner) + .send({ + name: `kick-test-group-${Date.now()}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + groupTest = res.body.group; + }); + + await inviteUser(testUserModerator._id); + + await request + .post(api('groups.addModerator')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserModerator._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + after(async () => { + await Promise.all([ + request + .post(api('groups.delete')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + }) + .expect('Content-Type', 'application/json') + .expect(200), + // updatePermission('kick-user-from-any-p-room', []), + updatePermission('remove-user', ['admin', 'owner', 'moderator']), + deleteUser(testUserModerator), + deleteUser(testUserOwner), + deleteUser(testUserMember), + ]); + }); + + it("should return an error when user is not a member of the group and doesn't have permission", async () => { + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }); + }); + + it('should allow a moderator to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsModerator) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should allow an owner to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it.skip('should kick user from group if not a member of the room but has the required permission', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: group._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it("should return an error when the owner doesn't have the required permission", async () => { + await updatePermission('remove-user', ['admin', 'moderator']); + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') - it('/groups.addOwner', (done) => { - request - .post(api('groups.addOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + }); + }); + + it('should return an error when trying to kick the last owner from a group', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserOwner._id, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-you-are-last-owner'); + }); + }); + + it('should return an error when trying to kick user that does not exist'); + it('should return an error when trying to kick user from a group that does not exist'); + it('should return an error when trying to kick user from a group that the user is not in the room'); }); describe('/groups.setDescription', () => { @@ -623,114 +798,124 @@ describe('[Groups]', function () { }); }); - it('/groups.archive', (done) => { - request - .post(api('groups.archive')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.archive', () => { + it('should archive the group', (done) => { + request + .post(api('groups.archive')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.unarchive', (done) => { - request - .post(api('groups.unarchive')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.unarchive', () => { + it('should unarchive the group', (done) => { + request + .post(api('groups.unarchive')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.close', (done) => { - request - .post(api('groups.close')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); + describe('/groups.close', () => { + it('should close the group', (done) => { + request + .post(api('groups.close')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); - it('/groups.close', (done) => { - request - .post(api('groups.close')) - .set(credentials) - .send({ - roomName: apiPrivateChannelName, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', `The private group, ${apiPrivateChannelName}, is already closed to the sender`); - }) - .end(done); + it('should return an error when trying to close a private group that is already closed', (done) => { + request + .post(api('groups.close')) + .set(credentials) + .send({ + roomName: apiPrivateChannelName, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `The private group, ${apiPrivateChannelName}, is already closed to the sender`); + }) + .end(done); + }); }); - it('/groups.open', (done) => { - request - .post(api('groups.open')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.open', () => { + it('should open the group', (done) => { + request + .post(api('groups.open')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.list', (done) => { - request - .get(api('groups.list')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('groups').and.to.be.an('array'); - }) - .end(done); - }); + describe('/groups.list', () => { + it('should list the groups the caller is part of', (done) => { + request + .get(api('groups.list')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('groups').and.to.be.an('array'); + }) + .end(done); + }); - it('/groups.list should return a list of zero length if not a member of any group', async () => { - const user = await createUser(); - const newCreds = await login(user.username, password); - request - .get(api('groups.list')) - .set(newCreds) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count').and.to.equal(0); - expect(res.body).to.have.property('total').and.to.equal(0); - expect(res.body).to.have.property('groups').and.to.be.an('array').and.that.has.lengthOf(0); - }); + it('should return a list of zero length if not a member of any group', async () => { + const user = await createUser(); + const newCreds = await login(user.username, password); + await request + .get(api('groups.list')) + .set(newCreds) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count').and.to.equal(0); + expect(res.body).to.have.property('total').and.to.equal(0); + expect(res.body).to.have.property('groups').and.to.be.an('array').and.that.has.lengthOf(0); + }); + }); }); - describe('[/groups.online]', () => { + describe('/groups.online', () => { const createUserAndChannel = async (setAsOnline = true) => { const testUser = await createUser(); const testUserCredentials = await login(testUser.username, password); @@ -812,7 +997,7 @@ describe('[Groups]', function () { const { room } = await createUserAndChannel(); - return request + await request .get(api('groups.online')) .set(outsiderCredentials) .query(`query={"_id": "${room._id}"}`) @@ -823,6 +1008,7 @@ describe('[Groups]', function () { }); }); }); + describe('/groups.members', () => { it('should return group members when searching by roomId', (done) => { request @@ -864,7 +1050,7 @@ describe('[Groups]', function () { }); }); - describe('[/groups.files]', async () => { + describe('/groups.files', async () => { await testFileUploads('groups.files', group); }); @@ -899,192 +1085,204 @@ describe('[Groups]', function () { }); }); - it('/groups.counters', (done) => { - request - .get(api('groups.counters')) - .set(credentials) - .query({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('joined', true); - expect(res.body).to.have.property('members'); - expect(res.body).to.have.property('unreads'); - expect(res.body).to.have.property('unreadsFrom'); - expect(res.body).to.have.property('msgs'); - expect(res.body).to.have.property('latest'); - expect(res.body).to.have.property('userMentions'); - }) - .end(done); + describe('/groups.counters', () => { + it('should return group counters', (done) => { + request + .get(api('groups.counters')) + .set(credentials) + .query({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('joined', true); + expect(res.body).to.have.property('members'); + expect(res.body).to.have.property('unreads'); + expect(res.body).to.have.property('unreadsFrom'); + expect(res.body).to.have.property('msgs'); + expect(res.body).to.have.property('latest'); + expect(res.body).to.have.property('userMentions'); + }) + .end(done); + }); }); - it('/groups.rename', async () => { - const roomInfo = await getRoomInfo(group._id); + describe('/groups.rename', async () => { + let roomInfo; + before(async () => { + roomInfo = await getRoomInfo(group._id); + }); - return request - .post(api('groups.rename')) - .set(credentials) - .send({ - roomId: group._id, - name: `EDITED${apiPrivateChannelName}`, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', `EDITED${apiPrivateChannelName}`); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); + it('should return the group rename with an additional message', async () => { + await request + .post(api('groups.rename')) + .set(credentials) + .send({ + roomId: group._id, + name: `EDITED${apiPrivateChannelName}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', `EDITED${apiPrivateChannelName}`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); + }); + }); }); describe('/groups.getIntegrations', () => { let integrationCreatedByAnUser; - let userCredentials; let createdGroup; - before((done) => { - createRoom({ name: `test-integration-group-${Date.now()}`, type: 'p' }).end((err, res) => { - createdGroup = res.body.group; - createUser().then((createdUser) => { - const user = createdUser; - login(user.username, password).then((credentials) => { - userCredentials = credentials; - updatePermission('manage-incoming-integrations', ['user']).then(() => { - updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - createIntegration( - { - type: 'webhook-incoming', - name: 'Incoming test', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: false, - overrideDestinationChannelEnabled: true, - channel: `#${createdGroup.name}`, - }, - userCredentials, - ).then((integration) => { - integrationCreatedByAnUser = integration; - done(); - }); - }); - }); - }); - }); - }); + + before(async () => { + const resRoom = await createRoom({ name: `test-integration-group-${Date.now()}`, type: 'p' }); + + createdGroup = resRoom.body.group; + + const user = await createUser(); + + const userCredentials = await login(user.username, password); + + await Promise.all([ + updatePermission('manage-incoming-integrations', ['user']), + updatePermission('manage-own-incoming-integrations', ['user']), + ]); + + integrationCreatedByAnUser = await createIntegration( + { + type: 'webhook-incoming', + name: 'Incoming test', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: false, + overrideDestinationChannelEnabled: true, + channel: `#${createdGroup.name}`, + }, + userCredentials, + ); }); - after((done) => { - removeIntegration(integrationCreatedByAnUser._id, 'incoming').then(done); + after(async () => { + await removeIntegration(integrationCreatedByAnUser._id, 'incoming'); + + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-outgoing-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-own-outgoing-integrations', ['admin']), + ]); }); - it('should return the list of integrations of create group and it should contain the integration created by user when the admin DOES have the permission', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( - (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, - ); - expect(integrationCreated).to.be.an('object'); - expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - }) - .end(done); - }); + it('should return the list of integrations of create group and it should contain the integration created by user when the admin DOES have the permission', async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const integrationCreated = res.body.integrations.find( + (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, + ); + expect(integrationCreated).to.be.an('object'); + expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + }); }); - it('should return the list of integrations created by the user only', (done) => { - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - updatePermission('manage-incoming-integrations', []).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( - (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, - ); - expect(integrationCreated).to.be.equal(undefined); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - }) - .end(done); + it('should return the list of integrations created by the user only', async () => { + await Promise.all([ + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-incoming-integrations', []), + ]); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const integrationCreated = res.body.integrations.find( + (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, + ); + expect(integrationCreated).to.be.equal(undefined); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); }); - }); }); - it('should return unauthorized error when the user does not have any integrations permissions', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', []).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(403) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'unauthorized'); - }) - .end(done); - }); - }); + it('should return unauthorized error when the user does not have any integrations permissions', async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-outgoing-integrations', []), + updatePermission('manage-own-incoming-integrations', []), + updatePermission('manage-own-outgoing-integrations', []), + ]); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'unauthorized'); }); - }); }); }); - it('/groups.setReadOnly', (done) => { - request - .post(api('groups.setReadOnly')) - .set(credentials) - .send({ - roomId: group._id, - readOnly: true, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.setReadOnly', () => { + it('should set the group as read only', (done) => { + request + .post(api('groups.setReadOnly')) + .set(credentials) + .send({ + roomId: group._id, + readOnly: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it.skip('/groups.leave', (done) => { - request - .post(api('groups.leave')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe.skip('/groups.leave', () => { + it('should allow the user to leave the group', (done) => { + request + .post(api('groups.leave')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); describe('/groups.setAnnouncement', () => { @@ -1123,39 +1321,101 @@ describe('[Groups]', function () { }); describe('/groups.setType', () => { - it('should change the type of the group to a channel', (done) => { - request + let roomTypeId; + + before(async () => { + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.type.${Date.now()}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + + roomTypeId = res.body.group._id; + }); + }); + + after(async () => { + await request + .post(api('channels.delete')) + .set(credentials) + .send({ + roomId: roomTypeId, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should change the type of the group to a channel', async () => { + await request .post(api('groups.setType')) .set(credentials) .send({ - roomId: group._id, + roomId: roomTypeId, type: 'c', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + expect(res.body).to.have.nested.property('group.t', 'c'); + }); }); }); - describe('/groups.setCustomFields:', () => { + describe('/groups.setCustomFields', () => { let cfchannel; - it('create group with customFields', (done) => { + let groupWithoutCustomFields; + + before('create group with customFields', async () => { const customFields = { field0: 'value0' }; - request + + await request .post(api('groups.create')) .set(credentials) .send({ name: `channel.cf.${Date.now()}`, customFields, }) - .end((err, res) => { + .expect((res) => { cfchannel = res.body.group; - done(); }); + + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.cf.${Date.now()}`, + }) + .expect((res) => { + groupWithoutCustomFields = res.body.group; + }); + }); + + after('delete group with customFields', async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: groupWithoutCustomFields.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); }); + it('get customFields using groups.info', (done) => { request .get(api('groups.info')) @@ -1173,7 +1433,7 @@ describe('[Groups]', function () { }); it('change customFields', async () => { const customFields = { field9: 'value9' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ @@ -1206,39 +1466,14 @@ describe('[Groups]', function () { }) .end(done); }); - it('delete group with customFields', (done) => { - request - .post(api('groups.delete')) - .set(credentials) - .send({ - roomName: cfchannel.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - it('create group without customFields', (done) => { - request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `channel.cf.${Date.now()}`, - }) - .end((err, res) => { - cfchannel = res.body.group; - done(); - }); - }); + it('set customFields with one nested field', async () => { const customFields = { field1: 'value1' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomId: cfchannel._id, + roomId: groupWithoutCustomFields._id, customFields, }) .expect('Content-Type', 'application/json') @@ -1246,7 +1481,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.customFields.field1', 'value1'); }); @@ -1254,11 +1489,11 @@ describe('[Groups]', function () { it('set customFields with multiple nested fields', async () => { const customFields = { field2: 'value2', field3: 'value3', field4: 'value4' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1266,7 +1501,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.customFields.field2', 'value2'); expect(res.body).to.have.nested.property('group.customFields.field3', 'value3'); @@ -1277,11 +1512,11 @@ describe('[Groups]', function () { it('set customFields to empty object', async () => { const customFields = {}; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1289,7 +1524,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.not.nested.property('group.customFields.field2', 'value2'); expect(res.body).to.have.not.nested.property('group.customFields.field3', 'value3'); @@ -1303,7 +1538,7 @@ describe('[Groups]', function () { .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1313,37 +1548,25 @@ describe('[Groups]', function () { }) .end(done); }); - it('delete group with empty customFields', (done) => { - request - .post(api('groups.delete')) - .set(credentials) - .send({ - roomName: cfchannel.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); }); describe('/groups.delete', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); - it('/groups.delete', (done) => { + + it('should delete group', (done) => { request .post(api('groups.delete')) .set(credentials) @@ -1357,7 +1580,8 @@ describe('[Groups]', function () { }) .end(done); }); - it('/groups.info', (done) => { + + it('should return group not found', (done) => { request .get(api('groups.info')) .set(credentials) @@ -1376,18 +1600,31 @@ describe('[Groups]', function () { describe('/groups.roles', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.roles.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + it('/groups.invite', (done) => { request .post(api('groups.invite')) @@ -1451,18 +1688,31 @@ describe('[Groups]', function () { describe('/groups.moderators', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.roles.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + it('/groups.invite', (done) => { request .post(api('groups.invite')) @@ -1503,17 +1753,35 @@ describe('[Groups]', function () { describe('/groups.setEncrypted', () => { let testGroup; - it('/groups.create', (done) => { - request + + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.encrypted.test.${Date.now()}`, }) - .end((err, res) => { + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + testGroup = res.body.group; - done(); }); + + await updateSetting('E2E_Enable', true); + }); + + after(async () => { + await updateSetting('E2E_Enable', false); + + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); }); it('should return an error when passing no boolean param', (done) => { @@ -1584,16 +1852,28 @@ describe('[Groups]', function () { }); describe('/groups.convertToTeam', () => { - before((done) => { - request + let newGroup; + + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group-${Date.now()}` }) .expect(200) .expect((response) => { - this.newGroup = response.body.group; + newGroup = response.body.group; + }); + }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: newGroup.name, }) - .then(() => done()); + .expect('Content-Type', 'application/json') + .expect(200); }); it('should fail to convert group if lacking edit-room permission', (done) => { @@ -1602,7 +1882,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(403) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1618,7 +1898,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(403) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1634,7 +1914,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(200) .expect((res) => { expect(res.body).to.have.a.property('success', true); @@ -1652,7 +1932,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(400) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1693,6 +1973,7 @@ describe('[Groups]', function () { expect(res.body).to.have.property('success', true); }); }); + after(async () => { await updateSetting('UI_Use_Real_Name', false); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 5a534fe2674d5..ed3c7eefb15b5 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -4,6 +4,7 @@ import path from 'path'; import { expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; +import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions.js'; @@ -1543,29 +1544,30 @@ describe('[Rooms]', function () { roomId = result.body.room.rid; }); - it('should update group name if user changes username', (done) => { - updateSetting('UI_Use_Real_Name', false).then(() => { - request - .post(api('users.update')) - .set(credentials) - .send({ - userId: testUser._id, - data: { - username: `changed.username.${testUser.username}`, - }, - }) - .end(() => { - request - .get(api('subscriptions.getOne')) - .set(credentials) - .query({ roomId }) - .end((err, res) => { - const { subscription } = res.body; - expect(subscription.name).to.equal(`rocket.cat,changed.username.${testUser.username}`); - done(); - }); - }); - }); + it('should update group name if user changes username', async () => { + await updateSetting('UI_Use_Real_Name', false); + await request + .post(api('users.update')) + .set(credentials) + .send({ + userId: testUser._id, + data: { + username: `changed.username.${testUser.username}`, + }, + }); + + // need to wait for the username update finish + await sleep(300); + + await request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ roomId }) + .send() + .expect((res) => { + const { subscription } = res.body; + expect(subscription.name).to.equal(`rocket.cat,changed.username.${testUser.username}`); + }); }); it('should update group name if user changes name', (done) => { diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index 032d250180666..899eb0db7d5fa 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -5,7 +5,7 @@ import { getCredentials, request, methodCall, api, credentials } from '../../dat import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; -import { createUser, deleteUser } from '../../data/users.helper.js'; +import { createUser, deleteUser, login } from '../../data/users.helper.js'; describe('Meteor.methods', function () { this.retries(0); @@ -2397,4 +2397,237 @@ describe('Meteor.methods', function () { }); }); }); + + describe('[@muteUserInRoom & @unmuteUserInRoom]', () => { + let rid = null; + let channelName = null; + let testUser = null; + let testUserCredentials = {}; + + before('create test user', async () => { + const username = `user.test.${Date.now()}`; + const email = `${username}@rocket.chat`; + + testUser = await createUser({ email, name: username, username, password: username, roles: ['user'] }); + }); + + before('create channel', async () => { + channelName = `methods-test-channel-${Date.now()}`; + rid = (await createRoom({ type: 'c', name: channelName, members: [testUser.username] })).body.channel._id; + }); + + before('login testUser', async () => { + testUserCredentials = await login(testUser.username, testUser.username); + }); + + describe('-> standard room', () => { + describe('- when muting a user in a standard room', () => { + it('should mute an user in a standard room', async () => { + await request + .post(methodCall('muteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'muteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('muted user should not be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + expect(res.body.error).to.equal('You_have_been_muted'); + }); + }); + }); + + describe('- when unmuting a user in a standard room', () => { + it('should unmute an user in a standard room', async () => { + await request + .post(methodCall('unmuteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'unmuteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('unmuted user should be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + }); + + describe('-> read-only room', () => { + before('set room to read-only', async () => { + await request + .post(api('channels.setReadOnly')) + .set(credentials) + .send({ + roomId: rid, + readOnly: true, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should not allow an user to send messages', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + expect(res.body.error).to.equal(`You can't send messages because the room is readonly.`); + }); + }); + + describe('- when unmuting a user in a read-only room', () => { + it('should unmute an user in a read-only room', async () => { + await request + .post(methodCall('unmuteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'unmuteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('unmuted user in read-only room should be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + + describe('- when muting a user in a read-only room', () => { + it('should mute an user in a read-only room', async () => { + await request + .post(methodCall('muteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'muteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('muted user in read-only room should not be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + }); + }); + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index 61a2719d9cba0..c12b875783ab7 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -1,12 +1,30 @@ +import { faker } from '@faker-js/faker'; +import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; +import moment from 'moment'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { addOrRemoveAgentFromDepartment, createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; +import { + closeOmnichannelRoom, + placeRoomOnHold, + sendAgentMessage, + sendMessage, + startANewLivechatRoomAndTakeIt, +} from '../../../data/livechat/rooms'; +import { createAnOnlineAgent } from '../../../data/livechat/users'; +import { sleep } from '../../../data/livechat/utils'; +import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting } from '../../../data/permissions.helper'; +import type { IUserCredentialsHeader } from '../../../data/user'; +import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - dashboards', function () { this.retries(0); + // This test is expected to take more time since we're simulating real time conversations to verify analytics + this.timeout(60000); before((done) => getCredentials(done)); @@ -14,6 +32,106 @@ describe('LIVECHAT - dashboards', function () { await updateSetting('Livechat_enabled', true); }); + let department: ILivechatDepartment; + const agents: { + credentials: IUserCredentialsHeader; + user: IUser & { username: string }; + }[] = []; + let avgClosedRoomChatDuration = 0; + + const inactivityTimeout = 3; + + const TOTAL_MESSAGES = { + min: 5, + max: 10, + }; + const DELAY_BETWEEN_MESSAGES = { + min: 1000, + max: (inactivityTimeout - 1) * 1000, + }; + const TOTAL_ROOMS = 7; + + const simulateRealtimeConversation = async (chatInfo: Awaited>[]) => { + const promises = chatInfo.map(async (info) => { + const { room, visitor } = info; + + // send a few messages + const numberOfMessages = Random.between(TOTAL_MESSAGES.min, TOTAL_MESSAGES.max); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of Array(numberOfMessages - 1).keys()) { + // flip a coin to decide who will send the message + const willSendFromAgent = Random.between(0, 1) === 1; + + if (willSendFromAgent) { + await sendAgentMessage(room._id); + } else { + await sendMessage(room._id, faker.lorem.sentence(), visitor.token); + } + + const delay = Random.between(DELAY_BETWEEN_MESSAGES.min, DELAY_BETWEEN_MESSAGES.max); + await sleep(delay); + } + + // Last message is always from visitor so that the chat doesn't get abandoned due to + // "Livechat_visitor_inactivity_timeout" setting + await sendMessage(room._id, faker.lorem.sentence(), visitor.token); + }); + + await Promise.all(promises); + }; + + before(async () => { + if (!IS_EE) { + return; + } + + await updateSetting('Livechat_visitor_inactivity_timeout', inactivityTimeout); + await updateSetting('Livechat_enable_business_hours', false); + + // create dummy test data for further tests + const { department: createdDept, agent: agent1 } = await createDepartmentWithAnOnlineAgent(); + department = createdDept; + + console.log('department', department.name); + + const agent2 = await createAnOnlineAgent(); + await addOrRemoveAgentFromDepartment(department._id, { agentId: agent2.user._id, username: agent2.user.username }, true); + agents.push(agent1); + agents.push(agent2); + + const roomCreationStart = moment(); + // start a few chats + const promises = Array.from(Array(TOTAL_ROOMS).keys()).map((i) => { + // 2 rooms by agent 1 + if (i < 2) { + return startANewLivechatRoomAndTakeIt({ departmentId: department._id, agent: agent1.credentials }); + } + return startANewLivechatRoomAndTakeIt({ departmentId: department._id, agent: agent2.credentials }); + }); + + const results = await Promise.all(promises); + + const chatInfo = results.map((result) => ({ room: result.room, visitor: result.visitor })); + + // simulate messages being exchanged between agents and visitors + await simulateRealtimeConversation(chatInfo); + + // put a chat on hold + await sendAgentMessage(chatInfo[1].room._id); + await placeRoomOnHold(chatInfo[1].room._id); + // close a chat + await closeOmnichannelRoom(chatInfo[4].room._id); + const room5ChatDuration = moment().diff(roomCreationStart, 'seconds'); + // close an abandoned chat + await sendAgentMessage(chatInfo[5].room._id); + await sleep(inactivityTimeout * 1000); // wait for the chat to be considered abandoned + await closeOmnichannelRoom(chatInfo[5].room._id); + const room6ChatDuration = moment().diff(roomCreationStart, 'seconds'); + + avgClosedRoomChatDuration = (room5ChatDuration + room6ChatDuration) / 2; + }); + describe('livechat/analytics/dashboards/conversation-totalizers', () => { const expectedMetrics = [ 'Total_conversations', @@ -25,7 +143,7 @@ describe('LIVECHAT - dashboards', function () { 'Total_visitors', ]; it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -33,7 +151,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an array of conversation totalizers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -47,12 +165,51 @@ describe('LIVECHAT - dashboards', function () { ); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/conversation-totalizers')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('totalizers'); + expect(result.body.totalizers).to.be.an('array'); + expect(result.body.totalizers).to.have.lengthOf(5); + + const expectedResult = [ + { title: 'Total_conversations', value: 7 }, + { title: 'Open_conversations', value: 4 }, + { title: 'On_Hold_conversations', value: 1 }, + // { title: 'Total_messages', value: 60 }, + { title: 'Total_visitors', value: 7 }, + ]; + + expectedResult.forEach((expected) => { + const resultItem = result.body.totalizers.find((item: any) => item.title === expected.title); + expect(resultItem).to.not.be.undefined; + expect(resultItem).to.have.property('value', expected.value); + }); + + const minMessages = TOTAL_MESSAGES.min * TOTAL_ROOMS; + const maxMessages = TOTAL_MESSAGES.max * TOTAL_ROOMS; + + const totalMessages = result.body.totalizers.find((item: any) => item.title === 'Total_messages'); + expect(totalMessages).to.not.be.undefined; + const totalMessagesValue = parseInt(totalMessages.value); + expect(totalMessagesValue).to.be.greaterThanOrEqual(minMessages); + expect(totalMessagesValue).to.be.lessThanOrEqual(maxMessages); + }); }); describe('livechat/analytics/dashboards/productivity-totalizers', () => { const expectedMetrics = ['Avg_response_time', 'Avg_first_response_time', 'Avg_reaction_time', 'Avg_of_waiting_time']; it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -60,7 +217,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an array of productivity totalizers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -74,12 +231,41 @@ describe('LIVECHAT - dashboards', function () { ); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/productivity-totalizers')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + + // const expected = [ + // // There's a bug in the code for calculation of these 3 values. + // // Due to which it always return 0 + // { title: 'Avg_response_time', value: '00:00:00' }, + // { title: 'Avg_first_response_time', value: '00:00:00' }, + // { title: 'Avg_reaction_time', value: '00:00:00' }, + + // { title: 'Avg_of_waiting_time', value: '00:00:03' }, // approx 3, 5 delta + // ]; + + const avgWaitingTime = result.body.totalizers.find((item: any) => item.title === 'Avg_of_waiting_time'); + expect(avgWaitingTime).to.not.be.undefined; + + const avgWaitingTimeValue = moment.duration(avgWaitingTime.value).asSeconds(); + expect(avgWaitingTimeValue).to.be.closeTo(DELAY_BETWEEN_MESSAGES.max / 1000, 5); + }); }); describe('livechat/analytics/dashboards/chats-totalizers', () => { const expectedMetrics = ['Total_abandoned_chats', 'Avg_of_abandoned_chats', 'Avg_of_chat_duration_time']; it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -87,7 +273,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an array of chats totalizers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -101,12 +287,45 @@ describe('LIVECHAT - dashboards', function () { ); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/chats-totalizers')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + const expected = [ + { title: 'Total_abandoned_chats', value: 1 }, + { title: 'Avg_of_abandoned_chats', value: '14%' }, + // { title: 'Avg_of_chat_duration_time', value: '00:00:01' }, + ]; + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('totalizers'); + expect(result.body.totalizers).to.be.an('array'); + + expected.forEach((expected) => { + const resultItem = result.body.totalizers.find((item: any) => item.title === expected.title); + expect(resultItem).to.not.be.undefined; + expect(resultItem).to.have.property('value', expected.value); + }); + + const resultAverageChatDuration = result.body.totalizers.find((item: any) => item.title === 'Avg_of_chat_duration_time'); + expect(resultAverageChatDuration).to.not.be.undefined; + + const resultAverageChatDurationValue = moment.duration(resultAverageChatDuration.value).asSeconds(); + expect(resultAverageChatDurationValue).to.be.closeTo(avgClosedRoomChatDuration, 5); // Keep a margin of 3 seconds + }); }); describe('livechat/analytics/dashboards/agents-productivity-totalizers', () => { const expectedMetrics = ['Busiest_time', 'Avg_of_available_service_time', 'Avg_of_service_time']; it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get( api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'), @@ -116,7 +335,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an array of agents productivity totalizers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get( api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'), @@ -132,11 +351,40 @@ describe('LIVECHAT - dashboards', function () { ); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/agents-productivity-totalizers')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + // [ + // { title: 'Busiest_time', value: '- -' }, + // { title: 'Avg_of_available_service_time', value: '00:00:00' }, + // { title: 'Avg_of_service_time', value: '00:00:16' } approx 17, 6 delta + // ], + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('totalizers'); + expect(result.body.totalizers).to.be.an('array'); + + const avgServiceTime = result.body.totalizers.find((item: any) => item.title === 'Avg_of_service_time'); + + expect(avgServiceTime).to.not.be.undefined; + const avgServiceTimeValue = moment.duration(avgServiceTime.value).asSeconds(); + const minChatDuration = (DELAY_BETWEEN_MESSAGES.min * TOTAL_MESSAGES.min) / 1000; + const maxChatDuration = (DELAY_BETWEEN_MESSAGES.max * TOTAL_MESSAGES.max) / 1000; + expect(avgServiceTimeValue).to.be.closeTo((minChatDuration + maxChatDuration) / 2, 10); + }); }); describe('livechat/analytics/dashboards/charts/chats', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -144,7 +392,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an array of productivity totalizers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -157,11 +405,35 @@ describe('LIVECHAT - dashboards', function () { expect(res.body).to.have.property('queued'); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/charts/chats')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + const expected = { + open: 4, + closed: 2, + queued: 0, + onhold: 1, + }; + + expect(result.body).to.have.property('success', true); + + Object.entries(expected).forEach(([key, value]) => { + expect(result.body).to.have.property(key, value); + }); + }); }); describe('livechat/analytics/dashboards/charts/chats-per-agent', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -169,7 +441,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an object with open and closed chats by agent', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -179,11 +451,39 @@ describe('LIVECHAT - dashboards', function () { expect(res.body).to.have.property('success', true); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/charts/chats-per-agent')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + const expected = { + agent0: { open: 1, closed: 0, onhold: 1 }, + agent1: { open: 3, closed: 2 }, + }; + + expect(result.body).to.have.property('success', true); + + const agent0 = result.body[agents[0].user.username as string]; + const agent1 = result.body[agents[1].user.username as string]; + + Object.entries(expected.agent0).forEach(([key, value]) => { + expect(agent0).to.have.property(key, value); + }); + Object.entries(expected.agent1).forEach(([key, value]) => { + expect(agent1).to.have.property(key, value); + }); + }); }); describe('livechat/analytics/dashboards/charts/agents-status', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/agents-status')) .set(credentials) @@ -191,7 +491,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an object with agents status metrics', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/agents-status')) .set(credentials) @@ -205,11 +505,36 @@ describe('LIVECHAT - dashboards', function () { expect(res.body).to.have.property('available'); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/charts/agents-status')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + // TODO: We can improve tests further by creating some agents with different status + const expected = { + offline: 0, + away: 0, + busy: 0, + available: 2, + }; + + expect(result.body).to.have.property('success', true); + + Object.entries(expected).forEach(([key, value]) => { + expect(result.body).to.have.property(key, value); + }); + }); }); describe('livechat/analytics/dashboards/charts/chats-per-department', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -217,7 +542,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an object with open and closed chats by department', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -227,11 +552,34 @@ describe('LIVECHAT - dashboards', function () { expect(res.body).to.have.property('success', true); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/charts/chats-per-department')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + const expected = { + department0: { open: 5, closed: 2 }, + }; + + expect(result.body).to.have.property('success', true); + + const department0 = result.body[department.name]; + + Object.entries(expected.department0).forEach(([key, value]) => { + expect(department0).to.have.property(key, value); + }); + }); }); describe('livechat/analytics/dashboards/charts/timings', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -239,7 +587,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an object with open and closed chats by department', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) @@ -258,11 +606,52 @@ describe('LIVECHAT - dashboards', function () { expect(res.body.chatDuration).to.have.property('longest'); }); }); + (IS_EE ? it : it.skip)('should return data with correct values', async () => { + const start = moment().subtract(1, 'days').toISOString(); + const end = moment().toISOString(); + + const result = await request + .get(api('livechat/analytics/dashboards/charts/timings')) + .query({ start, end, departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + + // const expected = { + // response: { avg: 0, longest: 0.207 }, // avg between delayBetweenMessage.min and delayBetweenMessage.max + // reaction: { avg: 0, longest: 0.221 }, // avg between delayBetweenMessage.min and delayBetweenMessage.max + // chatDuration: { avg: 0, longest: 0.18 }, // avg should be about avgClosedRoomChatDuration, and longest should be greater than avgClosedRoomChatDuration and within delta of 20 + // success: true, + // }; + + const maxChatDuration = (DELAY_BETWEEN_MESSAGES.max * TOTAL_MESSAGES.max) / 1000; + + const responseValues = result.body.response; + expect(responseValues).to.have.property('avg'); + expect(responseValues).to.have.property('longest'); + expect(responseValues.avg).to.be.closeTo((DELAY_BETWEEN_MESSAGES.min + DELAY_BETWEEN_MESSAGES.max) / 2000, 5); + expect(responseValues.longest).to.be.lessThan(maxChatDuration); + + const reactionValues = result.body.reaction; + expect(reactionValues).to.have.property('avg'); + expect(reactionValues).to.have.property('longest'); + expect(reactionValues.avg).to.be.closeTo((DELAY_BETWEEN_MESSAGES.min + DELAY_BETWEEN_MESSAGES.max) / 2000, 5); + expect(reactionValues.longest).to.be.lessThan(maxChatDuration); + + const chatDurationValues = result.body.chatDuration; + expect(chatDurationValues).to.have.property('avg'); + expect(chatDurationValues).to.have.property('longest'); + expect(chatDurationValues.avg).to.be.closeTo(avgClosedRoomChatDuration, 5); + expect(chatDurationValues.longest).to.be.greaterThan(avgClosedRoomChatDuration); + expect(chatDurationValues.longest).to.be.lessThan(avgClosedRoomChatDuration + 20); + }); }); describe('livechat/analytics/agent-overview', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/agent-overview')) .query({ from: '2020-01-01', to: '2020-01-02', name: 'Total_conversations' }) @@ -271,7 +660,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an "invalid-chart-name error" when the chart name is empty', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/agent-overview')) .query({ from: '2020-01-01', to: '2020-01-02', name: '' }) @@ -305,11 +694,37 @@ describe('LIVECHAT - dashboards', function () { expect(result.body.head).to.be.an('array'); expect(result.body.data).to.be.an('array'); }); + (IS_EE ? it : it.skip)('should return agent overview data with correct values', async () => { + const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD'); + const today = moment().startOf('day').format('YYYY-MM-DD'); + + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: yesterday, to: today, name: 'Total_conversations', departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + expect(result.body.data).to.have.lengthOf(2); + + const user1Data = result.body.data.find((data: any) => data.name === agents[0].user.username); + const user2Data = result.body.data.find((data: any) => data.name === agents[1].user.username); + + expect(user1Data).to.not.be.undefined; + expect(user2Data).to.not.be.undefined; + + expect(user1Data).to.have.property('value', '28.57%'); + expect(user2Data).to.have.property('value', '71.43%'); + }); }); describe('livechat/analytics/overview', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request .get(api('livechat/analytics/overview')) .query({ from: '2020-01-01', to: '2020-01-02', name: 'Conversations' }) @@ -318,7 +733,7 @@ describe('LIVECHAT - dashboards', function () { .expect(403); }); it('should return an "invalid-chart-name error" when the chart name is empty', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await request .get(api('livechat/analytics/overview')) .query({ from: '2020-01-01', to: '2020-01-02', name: '' }) @@ -351,5 +766,43 @@ describe('LIVECHAT - dashboards', function () { expect(result.body[0]).to.have.property('title', 'Total_conversations'); expect(result.body[0]).to.have.property('value', 0); }); + (IS_EE ? it : it.skip)('should return analytics overview data with correct values', async () => { + const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD'); + const today = moment().startOf('day').format('YYYY-MM-DD'); + + const result = await request + .get(api('livechat/analytics/overview')) + .query({ from: yesterday, to: today, name: 'Conversations', departmentId: department._id }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.be.an('array'); + + const expectedResult = [ + { title: 'Total_conversations', value: 7 }, + { title: 'Open_conversations', value: 4 }, + { title: 'On_Hold_conversations', value: 1 }, + // { title: 'Total_messages', value: 6 }, + // { title: 'Busiest_day', value: moment().format('dddd') }, // TODO: need to check y this return a day before + { title: 'Conversations_per_day', value: '3.50' }, + { title: 'Busiest_time', value: '- -' }, + ]; + + expectedResult.forEach((expected) => { + const resultItem = result.body.find((item: any) => item.title === expected.title); + expect(resultItem).to.not.be.undefined; + expect(resultItem).to.have.property('value', expected.value); + }); + + const minMessages = TOTAL_MESSAGES.min * TOTAL_ROOMS; + const maxMessages = TOTAL_MESSAGES.max * TOTAL_ROOMS; + + const totalMessages = result.body.find((item: any) => item.title === 'Total_messages'); + expect(totalMessages).to.not.be.undefined; + const totalMessagesValue = parseInt(totalMessages.value); + expect(totalMessagesValue).to.be.greaterThanOrEqual(minMessages); + expect(totalMessagesValue).to.be.lessThanOrEqual(maxMessages); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index a3ca544de20c1..55ef4402da391 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -2,7 +2,8 @@ import { faker } from '@faker-js/faker'; import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; -import type { Response } from 'supertest'; +import moment from 'moment'; +import { type Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; @@ -334,6 +335,27 @@ describe('LIVECHAT - visitors', function () { }); }); + it('should return visitor activity field when visitor was active on month', async () => { + // Activity is determined by a conversation in which an agent has engaged (sent a message) + // For a visitor to be considered active, they must have had a conversation in the last 30 days + const period = moment().format('YYYY-MM'); + const { visitor, room } = await startANewLivechatRoomAndTakeIt(); + // agent should send a message on the room + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + rid: room._id, + msg: 'test', + }, + }); + + const activeVisitor = await getLivechatVisitorByToken(visitor.token); + expect(activeVisitor).to.have.property('activity'); + expect(activeVisitor.activity).to.include(period); + }); + it("should return a 'error-removing-visitor' error when removeGuest's result is false", async () => { await request .delete(api('livechat/visitor/123')) diff --git a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts index 8b85b2a637842..0409971330404 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts @@ -1,4 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -254,4 +254,68 @@ import { IS_EE } from '../../../e2e/config/constants'; expect(updatedRoom).to.not.have.property('onHold'); }); }); + + describe('visitor abandonment feature', () => { + let room: IOmnichannelRoom; + + before(async () => { + await updateSetting('Livechat_abandoned_rooms_action', 'Livechat_close_chat'); + await updateSetting('Livechat_visitor_inactivity_timeout', 60); + }); + + it('should set predictedVisitorAbandonmentAt when agent sends a message', async () => { + const { room: newRoom } = await startANewLivechatRoomAndTakeIt(); + + room = newRoom; + + await sendAgentMessage(room._id); + + const updatedRoom = await getLivechatRoomInfo(room._id); + + const lastMessageTs = updatedRoom.responseBy?.lastMessageTs; + const firstResponseTs = updatedRoom.responseBy?.firstResponseTs; + const predictedVisitorAbandonmentAt = updatedRoom.omnichannel?.predictedVisitorAbandonmentAt; + + expect(predictedVisitorAbandonmentAt).to.not.be.undefined; + expect(lastMessageTs).to.not.be.undefined; + expect(firstResponseTs).to.not.be.undefined; + + // expect predictedVisitorAbandonmentAt to be 60 seconds after lastMessageTs + const lastMessageTsDate = new Date(lastMessageTs as Date); + const predictedVisitorAbandonmentAtDate = new Date(predictedVisitorAbandonmentAt as Date); + const firstResponseTsDate = new Date(firstResponseTs as Date); + + expect(predictedVisitorAbandonmentAtDate.getTime()).to.be.equal(lastMessageTsDate.getTime() + 60000); + expect(firstResponseTsDate.getTime()).to.be.equal(lastMessageTsDate.getTime()); + }); + + it('should not update predictedVisitorAbandonmentAt when agent sends yet another message', async () => { + await sendAgentMessage(room._id); + + const updatedRoom = await getLivechatRoomInfo(room._id); + + const lastMessageTs = updatedRoom.responseBy?.lastMessageTs; + const firstResponseTs = updatedRoom.responseBy?.firstResponseTs; + const predictedVisitorAbandonmentAt = updatedRoom.omnichannel?.predictedVisitorAbandonmentAt; + + expect(predictedVisitorAbandonmentAt).to.not.be.undefined; + expect(lastMessageTs).to.not.be.undefined; + + // expect predictedVisitorAbandonmentAt to be 60 seconds after first message + const lastMessageTsDate = new Date(lastMessageTs as Date); + const predictedVisitorAbandonmentAtDate = new Date(predictedVisitorAbandonmentAt as Date); + const firstResponseTsDate = new Date(firstResponseTs as Date); + + expect(predictedVisitorAbandonmentAtDate.getTime()).to.be.equal(firstResponseTsDate.getTime() + 60000); + + // lastMessageTs should be updated + expect(lastMessageTsDate.getTime()).to.not.be.equal(firstResponseTsDate.getTime()); + expect(lastMessageTsDate.getTime()).to.be.greaterThan(firstResponseTsDate.getTime()); + }); + + after(async () => { + await updateSetting('Livechat_abandoned_rooms_action', 'none'); + await updateSetting('Livechat_visitor_inactivity_timeout', 3600); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts b/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts index 9085e1cd388d0..0585c20bf1275 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts @@ -22,12 +22,12 @@ import { getDepartmentById, deleteDepartment, } from '../../../data/livechat/department'; -import { createAgent, makeAgentAvailable } from '../../../data/livechat/rooms'; +import { createAgent, createManager, makeAgentAvailable } from '../../../data/livechat/rooms'; import { removeAgent } from '../../../data/livechat/users'; import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting, updateEESetting } from '../../../data/permissions.helper'; import type { IUserCredentialsHeader } from '../../../data/user'; import { password } from '../../../data/user'; -import { createUser, deleteUser, getMe, login } from '../../../data/users.helper'; +import { setUserActiveStatus, createUser, deleteUser, getMe, getUserByUsername, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - business hours', function () { @@ -249,7 +249,7 @@ describe('LIVECHAT - business hours', function () { }); }); - (IS_EE ? describe : describe.skip)('[EE] BH operations upon creation', () => { + (IS_EE ? describe : describe.skip)('[EE][BH] On Business Hour created', () => { let defaultBusinessHour: ILivechatBusinessHour; before(async () => { @@ -288,7 +288,7 @@ describe('LIVECHAT - business hours', function () { // and "dep2" and both these depts are connected to same BH, then in this case after // archiving "dep1", we'd still need to BH within this user's cache since he's part of // "dep2" which is linked to BH - (IS_EE ? describe : describe.skip)('[EE] BH operations post department archiving', () => { + (IS_EE ? describe : describe.skip)('[EE][BH] On Department archived', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; @@ -434,7 +434,7 @@ describe('LIVECHAT - business hours', function () { await deleteUser(agentLinkedToDept.user); }); }); - (IS_EE ? describe : describe.skip)('[EE] BH operations post department disablement', () => { + (IS_EE ? describe : describe.skip)('[EE][BH] On Department disabled', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; @@ -578,7 +578,7 @@ describe('LIVECHAT - business hours', function () { await deleteUser(agentLinkedToDept.user); }); }); - (IS_EE ? describe : describe.skip)('[EE] BH operations post department removal', () => { + (IS_EE ? describe : describe.skip)('[EE][BH] On Department removed', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; @@ -702,7 +702,7 @@ describe('LIVECHAT - business hours', function () { await deleteUser(agentLinkedToDept.user); }); }); - describe('BH behavior upon new agent creation/deletion', () => { + describe('[CE][BH] On Agent created/removed', () => { let defaultBH: ILivechatBusinessHour; let agent: ILivechatAgent; let agentCredentials: IUserCredentialsHeader; @@ -782,4 +782,75 @@ describe('LIVECHAT - business hours', function () { await deleteUser(agent._id); }); }); + + describe('[CE][BH] On Agent deactivated/activated', () => { + let defaultBH: ILivechatBusinessHour; + let agent: ILivechatAgent; + + before(async () => { + await updateSetting('Livechat_enable_business_hours', true); + await updateEESetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.SINGLE); + // wait for callbacks to run + await sleep(2000); + + defaultBH = await getDefaultBusinessHour(); + await openOrCloseBusinessHour(defaultBH, true); + + agent = await createUser(); + await createAgent(agent.username); + }); + + after(async () => { + await deleteUser(agent); + await updateSetting('Livechat_enable_business_hours', false); + }); + + it('should verify if agent becomes unavailable to take chats when user is deactivated', async () => { + await setUserActiveStatus(agent._id, false); + + const latestAgent = await getUserByUsername(agent.username); + + expect(latestAgent).to.be.an('object'); + expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); + }); + + it('should verify if agent becomes available to take chats when user is activated, if business hour is active', async () => { + await openOrCloseBusinessHour(defaultBH, true); + + await setUserActiveStatus(agent._id, true); + + const latestAgent = await getUserByUsername(agent.username); + + expect(latestAgent).to.be.an('object'); + expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.AVAILABLE); + }); + it('should verify if agent becomes unavailable to take chats when user is activated, if business hour is inactive', async () => { + await openOrCloseBusinessHour(defaultBH, false); + + await setUserActiveStatus(agent._id, false); + await setUserActiveStatus(agent._id, true); + + const latestAgent = await getUserByUsername(agent.username); + + expect(latestAgent).to.be.an('object'); + expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); + }); + it('should verify if managers are not able to make deactivated agents available', async () => { + await createManager(); + + await setUserActiveStatus(agent._id, false); + + const response = await request + .post(api('livechat/agent.status')) + .set(credentials) + .send({ + status: 'available', + agentId: agent._id, + }) + .expect(400); + + expect(response.body).to.have.property('success', false); + expect(response.body).to.have.property('error', 'error-user-deactivated'); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts new file mode 100644 index 0000000000000..d4c2fd59211c5 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { before, it, describe } from 'mocha'; + +import { getCredentials, api, request } from '../../../data/api-data'; +import { addOrRemoveAgentFromDepartment, createDepartment } from '../../../data/livechat/department'; +import { + createAgent, + createLivechatRoom, + createManager, + createVisitor, + getLivechatRoomInfo, + makeAgentAvailable, +} from '../../../data/livechat/rooms'; +import { createMonitor, createUnit } from '../../../data/livechat/units'; +import { updateSetting, updatePermission } from '../../../data/permissions.helper'; +import { password } from '../../../data/user'; +import { createUser, login, setUserActiveStatus } from '../../../data/users.helper'; +import { IS_EE } from '../../../e2e/config/constants'; + +type TestUser = { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; + +(IS_EE ? describe : describe.skip)('Omnichannel - Monitors', () => { + let manager: TestUser; + let monitor: TestUser; + let noUnitDepartment: ILivechatDepartment; + let unitDepartment: ILivechatDepartment; + + before((done) => getCredentials(done)); + before(async () => { + await updateSetting('Livechat_accept_chats_with_no_agents', true); + await setUserActiveStatus('rocketchat.internal.admin.test', true); + await createAgent(); + await makeAgentAvailable(); + }); + before(async () => { + const user: IUser = await createUser(); + const userCredentials = await login(user.username, password); + if (!user.username) { + throw new Error('user not created'); + } + await createManager(user.username); + + manager = { + user, + credentials: userCredentials, + }; + }); + before(async () => { + const user: IUser = await createUser(); + const userCredentials = await login(user.username, password); + if (!user.username) { + throw new Error('user not created'); + } + await createMonitor(user.username); + + monitor = { + user, + credentials: userCredentials, + }; + }); + before(async () => { + noUnitDepartment = await createDepartment(); + unitDepartment = await createDepartment(); + + await createUnit(monitor.user._id, monitor.user.username!, [unitDepartment._id]); + }); + before(async () => { + await updatePermission('transfer-livechat-guest', ['admin', 'livechat-manager', 'livechat-agent', 'livechat-monitor']); + }); + + describe('Monitors & Rooms', () => { + it('should not return a room of a department that the monitor is not assigned to', async () => { + const visitor = await createVisitor(noUnitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .get(api('livechat/rooms')) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('rooms').that.is.an('array'); + expect(body.rooms.find((r: any) => r._id === room._id)).to.not.exist; + }); + it('should return a room of a department the monitor is assigned to', async () => { + const visitor = await createVisitor(unitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .get(api('livechat/rooms')) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('rooms').that.is.an('array'); + expect(body.rooms.find((r: any) => r._id === room._id)).to.exist; + }); + }); + + describe('Monitors & Departments', () => { + it('should not return a department that the monitor is not assigned to', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist; + }); + it('should return a department that the monitor is assigned to', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.length).to.be.equal(1); + expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist; + }); + it('should return both created departments to a manager', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true, sort: '{ "_updatedAt": 1 }' }) + .set(manager.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.exist; + expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist; + }); + it('should not return a department when monitor is only assigned as agent there', async () => { + await createAgent(monitor.user.username!); + await addOrRemoveAgentFromDepartment( + noUnitDepartment._id, + { agentId: monitor.user._id, username: monitor.user.username!, count: 0, order: 0 }, + true, + ); + + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.length).to.be.equal(1); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist; + }); + }); + + describe('Monitors & Forward', () => { + it('should successfully forward a room to another agent', async () => { + const visitor = await createVisitor(unitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/room.forward')) + .set(monitor.credentials) + .send({ + roomId: room._id, + userId: 'rocketchat.internal.admin.test', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('success', true); + + const room2 = await getLivechatRoomInfo(room._id); + + expect(room2).to.have.property('servedBy').that.is.an('object'); + expect(room2.servedBy).to.have.property('_id', 'rocketchat.internal.admin.test'); + }); + it('should successfully forward a room to a department', async () => { + const visitor = await createVisitor(noUnitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/room.forward')) + .set(monitor.credentials) + .send({ + roomId: room._id, + departmentId: unitDepartment._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('success', true); + + const room2 = await getLivechatRoomInfo(room._id); + expect(room2.departmentId).to.be.equal(unitDepartment._id); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/lib/server.tests.js b/apps/meteor/tests/unit/app/lib/server.tests.js deleted file mode 100644 index f77b5a4d1cb81..0000000000000 --- a/apps/meteor/tests/unit/app/lib/server.tests.js +++ /dev/null @@ -1,277 +0,0 @@ -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; - -const { default: PasswordPolicyClass } = proxyquire.noCallThru().load('../../../../app/lib/server/lib/PasswordPolicyClass', { - 'meteor/meteor': { - Meteor: { - absoluteUrl() { - return 'http://localhost:3000/'; - }, - }, - }, - '@rocket.chat/random': { - Random: { - id: () => 1, - }, - }, -}); - -describe('PasswordPolicyClass', () => { - describe('Default options', () => { - const passwordPolice = new PasswordPolicyClass(); - it('should be disabled', () => { - expect(passwordPolice.enabled).to.be.equal(false); - }); - it('should have minLength = -1', () => { - expect(passwordPolice.minLength).to.be.equal(-1); - }); - it('should have maxLength = -1', () => { - expect(passwordPolice.maxLength).to.be.equal(-1); - }); - it('should have forbidRepeatingCharacters = false', () => { - expect(passwordPolice.forbidRepeatingCharacters).to.be.false; - }); - it('should have forbidRepeatingCharactersCount = 3', () => { - expect(passwordPolice.forbidRepeatingCharactersCount).to.be.equal(3); - }); - it('should have mustContainAtLeastOneLowercase = false', () => { - expect(passwordPolice.mustContainAtLeastOneLowercase).to.be.false; - }); - it('should have mustContainAtLeastOneUppercase = false', () => { - expect(passwordPolice.mustContainAtLeastOneUppercase).to.be.false; - }); - it('should have mustContainAtLeastOneNumber = false', () => { - expect(passwordPolice.mustContainAtLeastOneNumber).to.be.false; - }); - it('should have mustContainAtLeastOneSpecialCharacter = false', () => { - expect(passwordPolice.mustContainAtLeastOneSpecialCharacter).to.be.false; - }); - - describe('Password tests with default options', () => { - it('should allow all passwords', () => { - const passwordPolice = new PasswordPolicyClass({ throwError: false }); - expect(passwordPolice.validate()).to.be.equal(false); - expect(passwordPolice.validate('')).to.be.equal(false); - expect(passwordPolice.validate(' ')).to.be.equal(false); - expect(passwordPolice.validate('a')).to.be.equal(true); - expect(passwordPolice.validate('aaaaaaaaa')).to.be.equal(true); - }); - }); - }); - - describe('Password tests with options', () => { - it('should not allow non string or empty', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - }); - expect(passwordPolice.validate()).to.be.false; - expect(passwordPolice.validate(1)).to.be.false; - expect(passwordPolice.validate(true)).to.be.false; - expect(passwordPolice.validate(new Date())).to.be.false; - expect(passwordPolice.validate(new Function())).to.be.false; - expect(passwordPolice.validate('')).to.be.false; - }); - - it('should restrict by minLength', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - minLength: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.false; - expect(passwordPolice.validate('1234')).to.be.false; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - }); - - it('should restrict by maxLength', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - maxLength: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - }); - - it('should allow repeated characters', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: false, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate('123456')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('11111111111111')).to.be.true; - }); - - it('should restrict repeated characters', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 3, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('11')).to.be.true; - expect(passwordPolice.validate('111')).to.be.true; - expect(passwordPolice.validate('1111')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - }); - - it('should restrict repeated characters customized', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('11')).to.be.true; - expect(passwordPolice.validate('111')).to.be.true; - expect(passwordPolice.validate('1111')).to.be.true; - expect(passwordPolice.validate('11111')).to.be.true; - expect(passwordPolice.validate('111111')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - }); - - it('should contain one lowercase', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneLowercase: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.true; - expect(passwordPolice.validate('aa')).to.be.true; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.true; - }); - - it('should contain one uppercase', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneUppercase: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.true; - expect(passwordPolice.validate('AAAaAAA')).to.be.true; - }); - - it('should contain one number', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneNumber: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.false; - expect(passwordPolice.validate('AAAa1AAA')).to.be.true; - }); - - it('should contain one special character', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneSpecialCharacter: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.false; - expect(passwordPolice.validate('AAAa1AAA')).to.be.false; - expect(passwordPolice.validate('AAAa@AAA')).to.be.true; - }); - }); - - describe('Password generator', () => { - it('should return a random password', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - }); - - expect(passwordPolice.generatePassword()).to.not.be.undefined; - }); - }); - - describe('Password Policy', () => { - it('should return a correct password policy', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - minLength: 10, - maxLength: 20, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 4, - mustContainAtLeastOneLowercase: true, - mustContainAtLeastOneUppercase: true, - mustContainAtLeastOneNumber: true, - mustContainAtLeastOneSpecialCharacter: true, - }); - - const policy = passwordPolice.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 passwordPolice = new PasswordPolicyClass({ - enabled: false, - }); - - const policy = passwordPolice.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 passwordPolice = new PasswordPolicyClass({ - enabled: true, - }); - - const policy = passwordPolice.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/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts b/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts new file mode 100644 index 0000000000000..b91165fb3ca90 --- /dev/null +++ b/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +// Create stubs for dependencies +const stubs = { + findOneUserById: sinon.stub(), + updateUsernameAndMessageOfMentionByIdAndOldUsername: sinon.stub(), + updateUsernameOfEditByUserId: sinon.stub(), + updateAllUsernamesByUserId: sinon.stub(), + updateDirectNameAndFnameByName: sinon.stub(), + updateUserReferences: sinon.stub(), + setUsername: sinon.stub(), + setRealName: sinon.stub(), + validateName: sinon.stub(), + FileUpload: sinon.stub(), +}; + +const { saveUserIdentity } = proxyquire.noCallThru().load('../../../../app/lib/server/functions/saveUserIdentity', { + '@rocket.chat/models': { + Users: { + findOneById: stubs.findOneUserById, + }, + Messages: { + updateUsernameAndMessageOfMentionByIdAndOldUsername: stubs.updateUsernameAndMessageOfMentionByIdAndOldUsername, + updateUsernameOfEditByUserId: stubs.updateUsernameOfEditByUserId, + updateAllUsernamesByUserId: stubs.updateAllUsernamesByUserId, + }, + Subscriptions: { + updateDirectNameAndFnameByName: stubs.updateDirectNameAndFnameByName, + }, + VideoConference: { + updateUserReferences: stubs.updateUserReferences, + }, + }, + 'meteor/meteor': { + 'Meteor': sinon.stub(), + '@global': true, + }, + '../../../../app/file-upload/server': { + FileUpload: stubs.FileUpload, + }, + '../../../../app/lib/server/functions/setRealName': { + _setRealName: stubs.setRealName, + }, + '../../../../app/lib/server/functions/setUsername': { + _setUsername: stubs.setUsername, + }, + '../../../../app/lib/server/functions/updateGroupDMsName': { + updateGroupDMsName: sinon.stub(), + }, + '../../../../app/lib/server/functions/validateName': { + validateName: stubs.validateName, + }, +}); + +describe('Users - saveUserIdentity', () => { + beforeEach(() => { + // Reset stubs before each test + Object.values(stubs).forEach((stub) => stub.reset()); + }); + + it('should return false if _id is not provided', async () => { + const result = await saveUserIdentity({ _id: undefined }); + + expect(stubs.findOneUserById.called).to.be.false; + expect(result).to.be.false; + }); + + it('should return false if user does not exist', async () => { + stubs.findOneUserById.returns(undefined); + const result = await saveUserIdentity({ _id: 'valid_id' }); + + expect(stubs.findOneUserById.calledWith('valid_id')).to.be.true; + expect(result).to.be.false; + }); + + it('should return false if username is not allowed', async () => { + stubs.findOneUserById.returns({ username: 'oldUsername' }); + stubs.validateName.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', username: 'admin' }); + + expect(stubs.validateName.calledWith('admin')).to.be.true; + expect(result).to.be.false; + }); + + it('should return false if username is invalid or unavailable', async () => { + stubs.findOneUserById.returns({ username: 'oldUsername' }); + stubs.validateName.returns(true); + stubs.setUsername.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', username: 'invalidUsername' }); + + expect(stubs.validateName.calledWith('invalidUsername')).to.be.true; + expect(stubs.setUsername.calledWith('valid_id', 'invalidUsername', { username: 'oldUsername' })).to.be.true; + expect(result).to.be.false; + }); + + it("should not update the username if it's not changed", async () => { + stubs.findOneUserById.returns({ username: 'oldUsername', name: 'oldName' }); + stubs.validateName.returns(true); + stubs.setUsername.returns(true); + await saveUserIdentity({ _id: 'valid_id', username: 'oldUsername', name: 'oldName' }); + + expect(stubs.validateName.called).to.be.false; + expect(stubs.setUsername.called).to.be.false; + expect(stubs.updateUsernameOfEditByUserId.called).to.be.false; + expect(stubs.updateAllUsernamesByUserId.called).to.be.false; + expect(stubs.updateUsernameAndMessageOfMentionByIdAndOldUsername.called).to.be.false; + expect(stubs.updateDirectNameAndFnameByName.called).to.be.false; + expect(stubs.updateUserReferences.called).to.be.false; + }); + + it('should return false if _setName fails', async () => { + stubs.findOneUserById.returns({ name: 'oldName' }); + stubs.setRealName.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', name: 'invalidName' }); + + expect(stubs.setRealName.calledWith('valid_id', 'invalidName', { name: 'oldName' })).to.be.true; + expect(result).to.be.false; + }); + + it('should update Subscriptions and VideoConference if name changes', async () => { + stubs.findOneUserById.returns({ name: 'oldName', username: 'oldUsername' }); + stubs.setRealName.returns(true); + const result = await saveUserIdentity({ _id: 'valid_id', name: 'name', username: 'oldUsername' }); + + expect(stubs.setUsername.called).to.be.false; + expect(stubs.setRealName.called).to.be.true; + expect(stubs.updateUsernameOfEditByUserId.called).to.be.false; + expect(stubs.updateDirectNameAndFnameByName.called).to.be.true; + expect(stubs.updateUserReferences.called).to.be.true; + expect(result).to.be.true; + }); +}); diff --git a/apps/meteor/tsconfig.typecheck.json b/apps/meteor/tsconfig.typecheck.json index e5a34906c8ce6..33699e98affb9 100644 --- a/apps/meteor/tsconfig.typecheck.json +++ b/apps/meteor/tsconfig.typecheck.json @@ -2,7 +2,12 @@ "extends": "./tsconfig.json", "compilerOptions": { "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "incremental": true, + "allowImportingTsExtensions": true, + "paths": { + "mongodb": ["node_modules/mongodb"] + } }, "exclude": [".meteor", "./packages", "./imports/client", "./ee/server/services", "public/", "private"] } diff --git a/codecov.yml b/codecov.yml index 0a3501678c1e1..2fe2eaf32b42c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,5 @@ +codecov: + max_report_age: off coverage: status: patch: off @@ -10,6 +12,13 @@ coverage: flags: - client flags: + unit: + carryforward: true + e2e: + paths: + - apps/meteor/ + carryforward: true + client: paths: - apps/meteor/client diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 67bb7a8164f50..81958cf94cc66 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,155 @@ # @rocket.chat/account-service +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.11-rc.3 + +## 0.2.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.0-rc.2 +- @rocket.chat/rest-typings@6.5.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.2.0-rc.2 +- @rocket.chat/models@0.0.11-rc.2 + +## 0.2.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index e59c9f672f8bc..d3dff1f3d8057 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -29,6 +40,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 8b6c0efc97960..9038b8cf2ef05 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", @@ -31,7 +31,7 @@ "gc-stats": "^1.4.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 882144f1602f3..e1f254d060536 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,155 @@ # @rocket.chat/authorization-service +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 + +## 0.2.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 0.2.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index e59c9f672f8bc..d3dff1f3d8057 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -29,6 +40,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 02431b537db56..8aff178aeecd2 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", @@ -30,7 +30,7 @@ "gc-stats": "^1.4.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2" diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 242f5b5170397..1bafaf49ea7eb 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,181 @@ # @rocket.chat/ddp-streamer +## 0.1.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- 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 [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/ui-contexts@2.0.0 + - @rocket.chat/models@0.0.15 + - @rocket.chat/instance-status@0.0.15 + +## 0.1.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/ui-contexts@2.0.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + - @rocket.chat/instance-status@0.0.15-rc.5 + +## 0.1.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/ui-contexts@2.0.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 +- @rocket.chat/instance-status@0.0.14-rc.4 + +## 0.1.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/ui-contexts@2.0.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 +- @rocket.chat/instance-status@0.0.13-rc.3 + +## 0.1.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/ui-contexts@2.0.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 +- @rocket.chat/instance-status@0.0.13-rc.2 + +## 0.1.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/ui-contexts@2.0.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 +- @rocket.chat/instance-status@0.0.11-rc.1 + +## 0.1.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- 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 [d45365436e] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/ui-contexts@2.0.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + - @rocket.chat/instance-status@0.0.11-rc.0 + +## 0.1.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/ui-contexts@1.0.7 +- @rocket.chat/models@0.0.13 +- @rocket.chat/instance-status@0.0.13 + +## 0.1.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/ui-contexts@1.0.6 +- @rocket.chat/models@0.0.12 +- @rocket.chat/instance-status@0.0.12 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/instance-status@0.0.11 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + - @rocket.chat/ui-contexts@1.0.5 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/instance-status@0.0.10 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + - @rocket.chat/ui-contexts@1.0.4 + ## 0.1.3 ### Patch Changes diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 83f1eb282c870..19fef1639db5e 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -16,21 +16,32 @@ COPY ./packages/core-typings/dist packages/core-typings/dist COPY ./packages/rest-typings/package.json packages/rest-typings/package.json COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/password-policies/package.json packages/password-policies/package.json +COPY ./packages/password-policies/dist packages/password-policies/dist + COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json COPY ./packages/ui-contexts/dist packages/ui-contexts/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist -COPY ./packages/instance-status/package.json packages/instance-status/package.json -COPY ./packages/instance-status/dist packages/instance-status/dist - COPY ./packages/logger/package.json packages/logger/package.json COPY ./packages/logger/dist packages/logger/dist +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + +COPY ./packages/instance-status/package.json packages/instance-status/package.json +COPY ./packages/instance-status/dist packages/instance-status/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -38,6 +49,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 32876be36fd8b..aed89a2d1c45b 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.1.3", + "version": "0.1.9", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", @@ -35,7 +35,7 @@ "jaeger-client": "^3.19.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2", diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 868ad8fec2026..bccb35d2b3263 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -72,6 +72,15 @@ export class DDPStreamer extends ServiceClass { return; } + metrics.register({ + name: 'rocketchat_subscription', + type: 'histogram', + labelNames: ['subscription'], + description: 'Client subscriptions to Rocket.Chat', + unit: 'millisecond', + quantiles: true, + }); + metrics.register({ name: 'users_connected', type: 'gauge', @@ -86,6 +95,8 @@ export class DDPStreamer extends ServiceClass { description: 'Users logged by streamer', }); + server.setMetrics(metrics); + server.on(DDP_EVENTS.CONNECTED, () => { metrics.increment('users_connected', { nodeID }, 1); }); diff --git a/ee/apps/ddp-streamer/src/Server.ts b/ee/apps/ddp-streamer/src/Server.ts index 01c7c63511ff1..af083621230db 100644 --- a/ee/apps/ddp-streamer/src/Server.ts +++ b/ee/apps/ddp-streamer/src/Server.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; +import type { IServiceMetrics } from '@rocket.chat/core-services'; import { MeteorService, isMeteorError, MeteorError } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import ejson from 'ejson'; @@ -38,6 +39,8 @@ export class Server extends EventEmitter { private _methods = new Map(); + private metrics?: IServiceMetrics; + public readonly id = uuidv1(); serialize = ejson.stringify; @@ -52,6 +55,10 @@ export class Server extends EventEmitter { return ejson.parse(payload); }; + setMetrics(metrics: IServiceMetrics): void { + this.metrics = metrics; + } + async call(client: Client, packet: IPacket): Promise { // if client is not connected we don't need to do anything if (client.ws.readyState !== WebSocket.OPEN) { @@ -103,9 +110,13 @@ export class Server extends EventEmitter { throw new MeteorError(404, `Subscription '${packet.name}' not found`); } + const end = this.metrics?.timer('rocketchat_subscription', { subscription: packet.name }); + const publication = new Publication(client, packet, this); const [eventName, options] = packet.params; await fn.call(publication, eventName, options); + + end?.(); } catch (err: unknown) { return this.nosub(client, packet, handleInternalException(err, 'Subscription error')); } diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index f7de2c547ab03..cb28d7d7a3ee0 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,139 @@ # @rocket.chat/omnichannel-transcript +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/tools@0.1.0 + - @rocket.chat/omnichannel-services@0.0.15 + - @rocket.chat/pdf-worker@0.0.15 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/tools@0.1.0-rc.0 + - @rocket.chat/omnichannel-services@0.0.15-rc.5 + - @rocket.chat/pdf-worker@0.0.15-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/omnichannel-services@0.0.14-rc.4 +- @rocket.chat/pdf-worker@0.0.14-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +## 0.2.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/omnichannel-services@0.0.13 +- @rocket.chat/pdf-worker@0.0.13 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-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/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 + +## 0.2.7-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/omnichannel-services@0.0.13-rc.2 +- @rocket.chat/pdf-worker@0.0.13-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 0.2.7-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/omnichannel-services@0.0.11-rc.1 +- @rocket.chat/pdf-worker@0.0.11-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.7-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/omnichannel-services@0.0.11-rc.0 + - @rocket.chat/pdf-worker@0.0.11-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/omnichannel-services@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/pdf-worker@0.0.10 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index cc27cd7de9b57..2c3c22a998c34 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist @@ -31,9 +42,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -41,6 +49,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 4ba11422a2bda..038055b4fb737 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", @@ -36,7 +36,7 @@ "moleculer": "^0.14.29", "moment-timezone": "^0.5.43", "mongo-message-queue": "^1.0.0", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2" diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 5bb3ef39b101f..f000298cbd189 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,150 @@ # @rocket.chat/presence-service +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [d9a150000d] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/presence@0.0.15 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/presence@0.0.15-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/presence@0.0.14-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +### Patch Changes + +- Updated dependencies [d9a150000d] + - @rocket.chat/presence@0.0.13-rc.3 + - @rocket.chat/core-typings@6.4.0-rc.3 + - @rocket.chat/core-services@0.2.0-rc.3 + - @rocket.chat/model-typings@0.1.0-rc.3 + - @rocket.chat/models@0.0.13-rc.3 + +## 0.2.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/presence@0.0.13-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 0.2.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/presence@0.0.11-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/presence@0.0.11-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies [c655be17ca] + - @rocket.chat/presence@0.0.13 + - @rocket.chat/core-typings@6.3.7 + - @rocket.chat/core-services@0.1.7 + - @rocket.chat/model-typings@0.0.13 + - @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/presence@0.0.12 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/presence@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/presence@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index 5db19b7c09001..9a056c4fde3de 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -22,9 +22,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json COPY ./packages/ui-contexts/dist packages/ui-contexts/dist @@ -35,6 +46,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index dfd9a8902aacd..87c76b4ff5c83 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", @@ -30,7 +30,7 @@ "gc-stats": "^1.4.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2" diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 1490afbb40ab5..a0abfb00c5c81 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,147 @@ # @rocket.chat/queue-worker +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/omnichannel-services@0.0.15 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/omnichannel-services@0.0.15-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/omnichannel-services@0.0.14-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/omnichannel-services@0.0.13-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 + +## 0.2.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/omnichannel-services@0.0.13-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 0.2.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/omnichannel-services@0.0.11-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/omnichannel-services@0.0.11-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/omnichannel-services@0.0.13 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/omnichannel-services@0.0.12 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.2.5 + +### Patch Changes + +- 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/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/omnichannel-services@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index cc27cd7de9b57..2c3c22a998c34 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist @@ -31,9 +42,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -41,6 +49,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 6907bfa478a83..156af4fdc0e60 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", @@ -34,7 +34,7 @@ "moleculer": "^0.14.29", "moment-timezone": "^0.5.43", "mongo-message-queue": "^1.0.0", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2" diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 6af9dfb7b0e0a..d76068538d9ce 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,136 @@ # @rocket.chat/stream-hub-service +## 0.2.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/models@0.0.15 + +## 0.2.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.8-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.13-rc.3 + +## 0.2.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.13-rc.2 + +## 0.2.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.2.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.2.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.2.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + ## 0.2.3 ### Patch Changes diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index 2bc54c2503893..d3dff1f3d8057 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -19,12 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist COPY ./packages/logger/package.json packages/logger/package.json COPY ./packages/logger/dist packages/logger/dist +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . @@ -32,6 +40,7 @@ COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/plugins .yarn/plugins COPY ./.yarn/releases .yarn/releases +COPY ./.yarn/patches .yarn/patches COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json ENV NODE_ENV=production \ diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 325a8a75f336e..cc29f058e9c99 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.2.3", + "version": "0.2.9", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", @@ -30,7 +30,7 @@ "gc-stats": "^1.4.0", "mem": "^8.1.1", "moleculer": "^0.14.29", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2" diff --git a/ee/packages/api-client/CHANGELOG.md b/ee/packages/api-client/CHANGELOG.md index 030d8cae3b993..cc600456d2587 100644 --- a/ee/packages/api-client/CHANGELOG.md +++ b/ee/packages/api-client/CHANGELOG.md @@ -1,5 +1,108 @@ # @rocket.chat/api-client +## 0.1.9 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + +## 0.1.9-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + +## 0.1.9-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 + +## 0.1.8-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 + +## 0.1.8-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 + +## 0.1.8-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 + +## 0.1.8-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + +## 0.1.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 + +## 0.1.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 + +## 0.1.5 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.5 +- @rocket.chat/rest-typings@6.3.5 + +## 0.1.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.4 +- @rocket.chat/rest-typings@6.3.4 + ## 0.1.3 ### Patch Changes diff --git a/ee/packages/api-client/package.json b/ee/packages/api-client/package.json index 488811d73155f..4827eddb734ad 100644 --- a/ee/packages/api-client/package.json +++ b/ee/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.1.3", + "version": "0.1.9", "devDependencies": { "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", diff --git a/ee/packages/ddp-client/CHANGELOG.md b/ee/packages/ddp-client/CHANGELOG.md index 6a656d38a488e..f623b9a729e65 100644 --- a/ee/packages/ddp-client/CHANGELOG.md +++ b/ee/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,117 @@ # @rocket.chat/ddp-client +## 0.2.0 + +### Minor Changes + +- 982ef6f459: Add new event to notify users directly about new banners + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [1041d4d361] +- Updated dependencies [9496f1eb97] +- Updated dependencies [93d4912e17] + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/api-client@0.1.9 + +## 0.2.0-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/api-client@0.1.9-rc.5 + +## 0.2.0-rc.4 + +### Patch Changes + +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/api-client@0.1.8-rc.4 + +## 0.2.0-rc.3 + +### Patch Changes + +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/api-client@0.1.8-rc.3 + +## 0.2.0-rc.2 + +### Patch Changes + +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/api-client@0.1.8-rc.2 + +## 0.2.0-rc.1 + +### Patch Changes + +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/api-client@0.1.8-rc.1 + +## 0.2.0-rc.0 + +### Minor Changes + +- 982ef6f459: Add new event to notify users directly about new banners + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [9496f1eb97] +- Updated dependencies [93d4912e17] + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/api-client@0.1.5-rc.0 + +## 0.1.8 + +### Patch Changes + +- @rocket.chat/rest-typings@6.3.8 +- @rocket.chat/api-client@0.1.8 + +## 0.1.7 + +### Patch Changes + +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/api-client@0.1.7 + +## 0.1.6 + +### Patch Changes + +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/api-client@0.1.6 + +## 0.1.5 + +### Patch Changes + +- @rocket.chat/rest-typings@6.3.5 +- @rocket.chat/api-client@0.1.5 + +## 0.1.4 + +### Patch Changes + +- @rocket.chat/rest-typings@6.3.4 +- @rocket.chat/api-client@0.1.4 + ## 0.1.3 ### Patch Changes diff --git a/ee/packages/ddp-client/package.json b/ee/packages/ddp-client/package.json index 9aeb554005d93..5f62047c24293 100644 --- a/ee/packages/ddp-client/package.json +++ b/ee/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.1.3", + "version": "0.2.0", "devDependencies": { "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", diff --git a/ee/packages/license/.eslintrc.json b/ee/packages/license/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /dev/null +++ b/ee/packages/license/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/license/__tests__/MockedLicenseBuilder.ts b/ee/packages/license/__tests__/MockedLicenseBuilder.ts new file mode 100644 index 0000000000000..4f2b49596be35 --- /dev/null +++ b/ee/packages/license/__tests__/MockedLicenseBuilder.ts @@ -0,0 +1,208 @@ +import { LicenseImp } from '../src'; +import type { ILicenseTag } from '../src/definition/ILicenseTag'; +import type { ILicenseV3 } from '../src/definition/ILicenseV3'; +import type { LicenseLimit } from '../src/definition/LicenseLimit'; +import type { LicenseModule } from '../src/definition/LicenseModule'; +import type { LicensePeriod, Timestamp } from '../src/definition/LicensePeriod'; +import { encrypt } from '../src/token'; + +export class MockedLicenseBuilder { + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: ILicenseTag[]; + }; + + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + + serverVersions?: { + value: string; + }[]; + + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + + constructor() { + this.information = { + autoRenew: true, + // expires in 1 year + visualExpiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + // 15 days before expiration + notifyAdminsAt: new Date(new Date().setDate(new Date().getDate() + 15)).toISOString(), + // 30 days before expiration + notifyUsersAt: new Date(new Date().setDate(new Date().getDate() + 30)).toISOString(), + trial: false, + offline: false, + createdAt: new Date().toISOString(), + grantedBy: { + method: 'manual', + seller: 'Rocket.Cat', + }, + tags: [ + { + name: 'Test', + color: 'blue', + }, + ], + }; + + this.validation = { + serverUrls: [ + { + value: 'localhost:3000', + type: 'url', + }, + ], + serverVersions: [ + { + value: '3.0.0', + }, + ], + + serverUniqueId: '1234567890', + cloudWorkspaceId: '1234567890', + + validPeriods: [ + { + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + validFrom: new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(), + validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + }, + ], + + statisticsReport: { + required: true, + allowedStaleInDays: 30, + }, + }; + } + + public resetValidPeriods(): this { + this.validation.validPeriods = []; + return this; + } + + public withValidPeriod(period: LicensePeriod): this { + this.validation.validPeriods.push(period); + return this; + } + + public withGrantedTo(grantedTo: { name?: string; company?: string; email?: string }): this { + this.information.grantedTo = grantedTo; + return this; + } + + grantedModules: { + module: LicenseModule; + }[]; + + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; + }; + + cloudMeta?: Record; + + public withServerUrls(urls: { value: string; type: 'url' | 'regex' | 'hash' }): this { + this.validation.serverUrls = this.validation.serverUrls ?? []; + this.validation.serverUrls.push(urls); + return this; + } + + public withServerVersions(versions: { value: string }): this { + this.validation.serverVersions = this.validation.serverVersions ?? []; + this.validation.serverVersions.push(versions); + return this; + } + + public withGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules.push(...modules.map((module) => ({ module }))); + return this; + } + + withNoGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules = this.grantedModules.filter(({ module }) => !modules.includes(module)); + return this; + } + + public withLimits(key: K, value: ILicenseV3['limits'][K]): this { + this.limits = this.limits ?? {}; + this.limits[key] = value; + return this; + } + + public build(): ILicenseV3 { + return { + version: '3.0', + information: this.information, + validation: this.validation, + grantedModules: [...new Set(this.grantedModules)], + limits: { + activeUsers: [], + guestUsers: [], + roomsPerGuest: [], + privateApps: [], + marketplaceApps: [], + monthlyActiveContacts: [], + ...this.limits, + }, + cloudMeta: this.cloudMeta, + }; + } + + public sign(): Promise { + return encrypt(this.build()); + } +} + +export const getReadyLicenseManager = async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + + license.setLicenseLimitCounter('activeUsers', () => 0); + license.setLicenseLimitCounter('guestUsers', () => 0); + license.setLicenseLimitCounter('roomsPerGuest', async () => 0); + license.setLicenseLimitCounter('privateApps', () => 0); + license.setLicenseLimitCounter('marketplaceApps', () => 0); + license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + return license; +}; diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts new file mode 100644 index 0000000000000..6147d12623bcc --- /dev/null +++ b/ee/packages/license/__tests__/emitter.spec.ts @@ -0,0 +1,119 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; + +describe('Event License behaviors', () => { + it('should call the module as they are enabled/disabled', async () => { + const license = await getReadyLicenseManager(); + const validFn = jest.fn(); + const invalidFn = jest.fn(); + + license.onValidFeature('livechat-enterprise', validFn); + license.onInvalidFeature('livechat-enterprise', invalidFn); + + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + // apply license + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(validFn).not.toBeCalled(); + await expect(invalidFn).toBeCalledTimes(1); + + // apply license containing livechat-enterprise module + + validFn.mockClear(); + invalidFn.mockClear(); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + + await expect(validFn).toBeCalledTimes(1); + await expect(invalidFn).toBeCalledTimes(0); + + // apply the old license again + + validFn.mockClear(); + invalidFn.mockClear(); + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(false); + await expect(validFn).toBeCalledTimes(0); + await expect(invalidFn).toBeCalledTimes(1); + }); + + it('should call `onValidateLicense` when a valid license is applied', async () => { + const license = await getReadyLicenseManager(); + const fn = jest.fn(); + + license.onValidateLicense(fn); + + const mocked = await new MockedLicenseBuilder(); + const token = await mocked.sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(fn).toBeCalledTimes(1); + }); + + describe('behavior:prevent_action event', () => { + it('should emit `behavior:prevent_action` event when the limit is reached', async () => { + const licenseManager = await getReadyLicenseManager(); + const fn = jest.fn(); + + licenseManager.onBehaviorTriggered('prevent_action', fn); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + await expect(fn).toBeCalledTimes(1); + + await expect(fn).toBeCalledWith({ + reason: 'limit', + limit: 'activeUsers', + }); + }); + + it('should emit `limitReached:activeUsers` event when the limit is reached', async () => { + const licenseManager = await getReadyLicenseManager(); + const fn = jest.fn(); + + licenseManager.onLimitReached('activeUsers', fn); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + await expect(fn).toBeCalledTimes(1); + + await expect(fn).toBeCalledWith(undefined); + }); + }); +}); diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts new file mode 100644 index 0000000000000..35a7a495edc0f --- /dev/null +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -0,0 +1,152 @@ +/** + * @jest-environment node + */ + +import { LicenseImp } from '../src'; +import { DuplicatedLicenseError } from '../src/errors/DuplicatedLicenseError'; +import { InvalidLicenseError } from '../src/errors/InvalidLicenseError'; +import { NotReadyForValidation } from '../src/errors/NotReadyForValidation'; +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; + +// Same license used on ci tasks so no I didnt leak it +const VALID_LICENSE = + 'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY='; + +describe('License set license procedures', () => { + describe('Invalid formats', () => { + it('should have no license by default', async () => { + const license = new LicenseImp(); + + expect(license.hasValidLicense()).toBe(false); + expect(license.getLicense()).toBeUndefined(); + }); + + it('should throw an error if the license applied is empty', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('')).rejects.toThrow(InvalidLicenseError); + }); + + it('should throw an error if the license applied is invalid', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + }); + }); + + it('should throw an error if the license is duplicated', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); + }); + + it('should keep a valid license if a new invalid formatted license is applied', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + await expect(license.hasValidLicense()).toBe(true); + }); + + describe('Pending cases', () => { + it('should return an error if the license is not ready for validation yet - missing workspace url', async () => { + const license = new LicenseImp(); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); + }); + + it('should return an error if the license is not ready for validation yet - missing counters', async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + + expect(license.getWorkspaceUrl()).toBe('localhost:3000'); + + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); + + await expect(license.hasValidLicense()).toBe(false); + }); + + it('should return a valid license if the license is ready for validation', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + }); + + describe('License V3', () => { + it('should return a valid license if the license is ready for validation', async () => { + const license = await getReadyLicenseManager(); + const token = await new MockedLicenseBuilder().sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + + it('should accept new licenses', async () => { + const license = await getReadyLicenseManager(); + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + }); + + it('should call a validated event after set a valid license', async () => { + const license = await getReadyLicenseManager(); + const validateCallback = jest.fn(); + license.onValidateLicense(validateCallback); + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + expect(validateCallback).toBeCalledTimes(1); + }); + + describe('License limits', () => { + describe('invalidate license', () => { + it('should trigger an invalidation event when a license with invalid limits is set after a valid one', async () => { + const invalidationCallback = jest.fn(); + + const licenseManager = await getReadyLicenseManager(); + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked + .withLimits('activeUsers', [ + { + max: 10, + behavior: 'invalidate_license', + }, + ]) + .sign(); + + const newToken = await mocked + .withLimits('activeUsers', [ + { + max: 1, + behavior: 'invalidate_license', + }, + ]) + .sign(); + + licenseManager.onInvalidateLicense(invalidationCallback); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + + await expect(licenseManager.setLicense(oldToken)).resolves.toBe(true); + await expect(licenseManager.hasValidLicense()).toBe(true); + + await expect(licenseManager.setLicense(newToken)).resolves.toBe(true); + await expect(licenseManager.hasValidLicense()).toBe(false); + + await expect(invalidationCallback).toBeCalledTimes(1); + }); + }); + }); + }); +}); diff --git a/ee/packages/license/babel.config.json b/ee/packages/license/babel.config.json new file mode 100644 index 0000000000000..e154c08135306 --- /dev/null +++ b/ee/packages/license/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-typescript"], + "plugins": [ + [ + "transform-inline-environment-variables", + { + "include": ["LICENSE_PUBLIC_KEY_V3"] + } + ] + ] +} diff --git a/ee/packages/license/jest.config.ts b/ee/packages/license/jest.config.ts new file mode 100644 index 0000000000000..21121603f6e0e --- /dev/null +++ b/ee/packages/license/jest.config.ts @@ -0,0 +1,16 @@ +export default { + preset: 'ts-jest', + errorOnDeprecated: true, + modulePathIgnorePatterns: ['/dist/'], + testMatch: ['**/**.spec.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + // transformIgnorePatterns: ['!node_modules/jose'], + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + '^jose$': require.resolve('jose'), + }, + collectCoverage: true, + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], +}; diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json new file mode 100644 index 0000000000000..6810f53e40ddf --- /dev/null +++ b/ee/packages/license/package.json @@ -0,0 +1,49 @@ +{ + "name": "@rocket.chat/license", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@babel/cli": "^7.23.0", + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.22.20", + "@babel/preset-typescript": "^7.23.0", + "@swc/core": "^1.3.66", + "@swc/jest": "^0.2.26", + "@types/babel__core": "^7", + "@types/babel__preset-env": "^7", + "@types/bcrypt": "^5.0.0", + "@types/jest": "~29.5.3", + "@types/ws": "^8.5.5", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "jest-environment-jsdom": "~29.6.1", + "jest-websocket-mock": "^2.4.0", + "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", + "test": "jest", + "testunit": "jest", + "build": "npm run build:types && npm run build:js", + "build:types": "tsc --emitDeclarationOnly", + "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/jwt": "workspace:^", + "@rocket.chat/logger": "workspace:^", + "bcrypt": "^5.0.1" + } +} diff --git a/apps/meteor/ee/app/license/definition/ILicenseTag.ts b/ee/packages/license/src/definition/ILicenseTag.ts similarity index 100% rename from apps/meteor/ee/app/license/definition/ILicenseTag.ts rename to ee/packages/license/src/definition/ILicenseTag.ts diff --git a/apps/meteor/ee/app/license/definition/ILicense.ts b/ee/packages/license/src/definition/ILicenseV2.ts similarity index 93% rename from apps/meteor/ee/app/license/definition/ILicense.ts rename to ee/packages/license/src/definition/ILicenseV2.ts index 7ac4bafdc7b53..57d921a249075 100644 --- a/apps/meteor/ee/app/license/definition/ILicense.ts +++ b/ee/packages/license/src/definition/ILicenseV2.ts @@ -1,6 +1,6 @@ import type { ILicenseTag } from './ILicenseTag'; -export interface ILicense { +export interface ILicenseV2 { url: string; expiry: string; maxActiveUsers: number; diff --git a/ee/packages/license/src/definition/ILicenseV3.ts b/ee/packages/license/src/definition/ILicenseV3.ts new file mode 100644 index 0000000000000..d3a2d7f572a37 --- /dev/null +++ b/ee/packages/license/src/definition/ILicenseV3.ts @@ -0,0 +1,64 @@ +import type { ILicenseTag } from './ILicenseTag'; +import type { LicenseLimit } from './LicenseLimit'; +import type { LicenseModule } from './LicenseModule'; +import type { LicensePeriod, Timestamp } from './LicensePeriod'; + +export interface ILicenseV3 { + version: '3.0'; + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: ILicenseTag[]; + }; + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + serverVersions?: { + value: string; + }[]; + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + grantedModules: { + module: LicenseModule; + }[]; + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; + }; + cloudMeta?: Record; +} + +export type LicenseLimitKind = keyof ILicenseV3['limits']; diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts new file mode 100644 index 0000000000000..8b5af5f3c4817 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -0,0 +1,17 @@ +import type { LicenseLimitKind } from './ILicenseV3'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; + +export type BehaviorWithContext = + | { + behavior: LicenseBehavior; + modules?: LicenseModule[]; + reason: 'limit'; + limit?: LicenseLimitKind; + } + | { + behavior: LicenseBehavior; + modules?: LicenseModule[]; + reason: 'period' | 'url'; + }; diff --git a/ee/packages/license/src/definition/LicenseLimit.ts b/ee/packages/license/src/definition/LicenseLimit.ts new file mode 100644 index 0000000000000..40e5a62f597a0 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseLimit.ts @@ -0,0 +1,7 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseLimit = { + max: number; + behavior: T; +} & (T extends 'disable_modules' ? { behavior: T; modules: LicenseModule[] } : { behavior: T }); diff --git a/ee/packages/license/src/definition/LicenseModule.ts b/ee/packages/license/src/definition/LicenseModule.ts new file mode 100644 index 0000000000000..8ecebba1983bc --- /dev/null +++ b/ee/packages/license/src/definition/LicenseModule.ts @@ -0,0 +1,18 @@ +export type LicenseModule = + | 'auditing' + | 'canned-responses' + | 'ldap-enterprise' + | 'livechat-enterprise' + | 'voip-enterprise' + | 'omnichannel-mobile-enterprise' + | 'engagement-dashboard' + | 'push-privacy' + | 'scalability' + | 'teams-mention' + | 'saml-enterprise' + | 'oauth-enterprise' + | 'device-management' + | 'federation' + | 'videoconference-enterprise' + | 'message-read-receipt' + | 'outlook-calendar'; diff --git a/ee/packages/license/src/definition/LicensePeriod.ts b/ee/packages/license/src/definition/LicensePeriod.ts new file mode 100644 index 0000000000000..d9bae6198fdec --- /dev/null +++ b/ee/packages/license/src/definition/LicensePeriod.ts @@ -0,0 +1,13 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type Timestamp = string; + +export type LicensePeriod = { + validFrom?: Timestamp; + validUntil?: Timestamp; + invalidBehavior: LicenseBehavior; +} & ({ validFrom: Timestamp } | { validUntil: Timestamp }) & + ({ invalidBehavior: 'disable_modules'; modules: LicenseModule[] } | { invalidBehavior: Exclude }); + +export type LicensePeriodBehavior = Exclude; diff --git a/ee/packages/license/src/definition/LicenseValidationOptions.ts b/ee/packages/license/src/definition/LicenseValidationOptions.ts new file mode 100644 index 0000000000000..6aa1e4213c623 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseValidationOptions.ts @@ -0,0 +1,11 @@ +import type { LicenseLimitKind } from './ILicenseV3'; +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LimitContext } from './LimitContext'; + +export type LicenseValidationOptions = { + behaviors?: LicenseBehavior[]; + limits?: LicenseLimitKind[]; + suppressLog?: boolean; + isNewLicense?: boolean; + context?: Partial<{ [K in LicenseLimitKind]: Partial> }>; +}; diff --git a/ee/packages/license/src/definition/LimitContext.ts b/ee/packages/license/src/definition/LimitContext.ts new file mode 100644 index 0000000000000..9dfc6d36be7fe --- /dev/null +++ b/ee/packages/license/src/definition/LimitContext.ts @@ -0,0 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import type { LicenseLimitKind } from './ILicenseV3'; + +export type LimitContext = { extraCount?: number } & (T extends 'roomsPerGuest' + ? { userId: IUser['_id'] } + : Record); diff --git a/ee/packages/license/src/definition/events.ts b/ee/packages/license/src/definition/events.ts new file mode 100644 index 0000000000000..53f3afe846dbe --- /dev/null +++ b/ee/packages/license/src/definition/events.ts @@ -0,0 +1,15 @@ +import type { LicenseLimitKind } from './ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>; +type BehaviorTriggered = Record<`behavior:${LicenseBehavior}`, { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }>; +type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>; + +export type LicenseEvents = ModuleValidation & + BehaviorTriggered & + LimitReached & { + validate: undefined; + invalidate: undefined; + module: { module: LicenseModule; valid: boolean }; + }; diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts new file mode 100644 index 0000000000000..65851a79c7eb8 --- /dev/null +++ b/ee/packages/license/src/deprecated.ts @@ -0,0 +1,38 @@ +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; +import { getModules } from './modules'; + +const getLicenseLimit = (license: ILicenseV3 | undefined, kind: LicenseLimitKind) => { + if (!license) { + return; + } + + const limitList = license.limits[kind]; + if (!limitList?.length) { + return; + } + + return Math.min(...limitList.map(({ max }) => max)); +}; + +// #TODO: Remove references to those functions + +export function getMaxActiveUsers(this: LicenseManager) { + return getLicenseLimit(this.getLicense(), 'activeUsers') ?? 0; +} + +export function getAppsConfig(this: LicenseManager) { + return { + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? -1, + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? -1, + }; +} + +export function getUnmodifiedLicenseAndModules(this: LicenseManager) { + if (this.valid && this.unmodifiedLicense) { + return { + license: this.unmodifiedLicense, + modules: getModules.call(this), + }; + } +} diff --git a/ee/packages/license/src/errors/DuplicatedLicenseError.ts b/ee/packages/license/src/errors/DuplicatedLicenseError.ts new file mode 100644 index 0000000000000..70b962d531058 --- /dev/null +++ b/ee/packages/license/src/errors/DuplicatedLicenseError.ts @@ -0,0 +1,6 @@ +export class DuplicatedLicenseError extends Error { + constructor(message = 'Duplicated license') { + super(message); + this.name = 'DuplicatedLicense'; + } +} diff --git a/ee/packages/license/src/errors/InvalidLicenseError.ts b/ee/packages/license/src/errors/InvalidLicenseError.ts new file mode 100644 index 0000000000000..a1eb328acd46c --- /dev/null +++ b/ee/packages/license/src/errors/InvalidLicenseError.ts @@ -0,0 +1,6 @@ +export class InvalidLicenseError extends Error { + constructor(message = 'Invalid license') { + super(message); + this.name = 'InvalidLicenseError'; + } +} diff --git a/ee/packages/license/src/errors/NotReadyForValidation.ts b/ee/packages/license/src/errors/NotReadyForValidation.ts new file mode 100644 index 0000000000000..ccb99e054500f --- /dev/null +++ b/ee/packages/license/src/errors/NotReadyForValidation.ts @@ -0,0 +1,6 @@ +export class NotReadyForValidation extends Error { + constructor(message = 'Not ready for validation') { + super(message); + this.name = 'NotReadyForValidation'; + } +} diff --git a/ee/packages/license/src/events/deprecated.ts b/ee/packages/license/src/events/deprecated.ts new file mode 100644 index 0000000000000..8ebfe4729292c --- /dev/null +++ b/ee/packages/license/src/events/deprecated.ts @@ -0,0 +1,12 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { hasModule } from '../modules'; + +// #TODO: Remove this onLicense handler +export function onLicense(this: LicenseManager, feature: LicenseModule, cb: (...args: any[]) => void): void | Promise { + if (hasModule.call(this, feature)) { + return cb(); + } + + this.once(`valid:${feature}`, cb); +} diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts new file mode 100644 index 0000000000000..9256bcafe5f75 --- /dev/null +++ b/ee/packages/license/src/events/emitter.ts @@ -0,0 +1,65 @@ +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; + +export function moduleValidated(this: LicenseManager, module: LicenseModule) { + try { + this.emit('module', { module, valid: true }); + this.emit(`valid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module added event', error }); + } +} + +export function moduleRemoved(this: LicenseManager, module: LicenseModule) { + try { + this.emit('module', { module, valid: false }); + this.emit(`invalid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module removed event', error }); + } +} + +export function behaviorTriggered(this: LicenseManager, options: BehaviorWithContext) { + const { behavior, reason, modules: _, ...rest } = options; + + try { + this.emit(`behavior:${behavior}`, { + reason, + ...rest, + }); + } catch (error) { + logger.error({ msg: 'Error running behavior triggered event', error }); + } + + if (behavior !== 'prevent_action') { + return; + } + + if (reason !== 'limit' || !(`limit` in rest) || !rest.limit) { + return; + } + + try { + this.emit(`limitReached:${rest.limit}`); + } catch (error) { + logger.error({ msg: 'Error running limit reached event', error }); + } +} + +export function licenseValidated(this: LicenseManager) { + try { + this.emit('validate'); + } catch (error) { + logger.error({ msg: 'Error running license validated event', error }); + } +} + +export function licenseInvalidated(this: LicenseManager) { + try { + this.emit('invalidate'); + } catch (error) { + logger.error({ msg: 'Error running license invalidated event', error }); + } +} diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts new file mode 100644 index 0000000000000..ecabecb28c0fd --- /dev/null +++ b/ee/packages/license/src/events/listeners.ts @@ -0,0 +1,84 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { hasModule } from '../modules'; + +export function onValidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`valid:${feature}`, cb); + + if (hasModule.call(this, feature)) { + cb(); + } + + return (): void => { + this.off(`valid:${feature}`, cb); + }; +} + +export function onInvalidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`invalid:${feature}`, cb); + + if (!hasModule.call(this, feature)) { + cb(); + } + + return (): void => { + this.off(`invalid:${feature}`, cb); + }; +} + +export function onToggledFeature( + this: LicenseManager, + feature: LicenseModule, + { up, down }: { up?: () => Promise | void; down?: () => Promise | void }, +): () => void { + let enabled = hasModule.call(this, feature); + + const offValidFeature = onValidFeature.bind(this)(feature, () => { + if (!enabled) { + void up?.(); + enabled = true; + } + }); + + const offInvalidFeature = onInvalidFeature.bind(this)(feature, () => { + if (enabled) { + void down?.(); + enabled = false; + } + }); + + if (enabled) { + void up?.(); + } + + return (): void => { + offValidFeature(); + offInvalidFeature(); + }; +} + +export function onModule(this: LicenseManager, cb: (data: { module: LicenseModule; valid: boolean }) => void) { + this.on('module', cb); +} + +export function onValidateLicense(this: LicenseManager, cb: () => void) { + this.on('validate', cb); +} + +export function onInvalidateLicense(this: LicenseManager, cb: () => void) { + this.on('invalidate', cb); +} + +export function onBehaviorTriggered( + this: LicenseManager, + behavior: Exclude, + cb: (data: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }) => void, +) { + this.on(`behavior:${behavior}`, cb); +} + +export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: () => void) { + this.on(`limitReached:${limitKind}`, cb); +} diff --git a/ee/packages/license/src/events/overwriteClassOnLicense.ts b/ee/packages/license/src/events/overwriteClassOnLicense.ts new file mode 100644 index 0000000000000..00a690d8f4134 --- /dev/null +++ b/ee/packages/license/src/events/overwriteClassOnLicense.ts @@ -0,0 +1,26 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { onLicense } from './deprecated'; + +interface IOverrideClassProperties { + [key: string]: (...args: any[]) => any; +} + +type Class = { new (...args: any[]): any }; + +export async function overwriteClassOnLicense( + this: LicenseManager, + + license: LicenseModule, + original: Class, + overwrite: IOverrideClassProperties, +): Promise { + await onLicense.call(this, license, () => { + Object.entries(overwrite).forEach(([key, value]) => { + const originalFn = original.prototype[key]; + original.prototype[key] = function (...args: any[]): any { + return value.call(this, originalFn, ...args); + }; + }); + }); +} diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts new file mode 100644 index 0000000000000..77e2976f156a7 --- /dev/null +++ b/ee/packages/license/src/index.ts @@ -0,0 +1,118 @@ +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseModule } from './definition/LicenseModule'; +import type { LimitContext } from './definition/LimitContext'; +import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; +import { onLicense } from './events/deprecated'; +import { + onBehaviorTriggered, + onInvalidFeature, + onInvalidateLicense, + onLimitReached, + onModule, + onToggledFeature, + onValidFeature, + onValidateLicense, +} from './events/listeners'; +import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; +import { LicenseManager } from './license'; +import { getModules, hasModule } from './modules'; +import { getTags } from './tags'; +import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; +import { validateFormat } from './validation/validateFormat'; + +export * from './definition/ILicenseTag'; +export * from './definition/ILicenseV2'; +export * from './definition/ILicenseV3'; +export * from './definition/LicenseBehavior'; +export * from './definition/LicenseLimit'; +export * from './definition/LicenseModule'; +export * from './definition/LicensePeriod'; +export * from './definition/LimitContext'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface License { + validateFormat: typeof validateFormat; + hasModule: typeof hasModule; + getModules: typeof getModules; + getTags: typeof getTags; + overwriteClassOnLicense: typeof overwriteClassOnLicense; + setLicenseLimitCounter: typeof setLicenseLimitCounter; + getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; + isLimitReached: (action: T, context?: Partial>) => Promise; + onValidFeature: typeof onValidFeature; + onInvalidFeature: typeof onInvalidFeature; + onToggledFeature: typeof onToggledFeature; + onModule: typeof onModule; + onValidateLicense: typeof onValidateLicense; + onInvalidateLicense: typeof onInvalidateLicense; + onLimitReached: typeof onLimitReached; + onBehaviorTriggered: typeof onBehaviorTriggered; + revalidateLicense: () => Promise; + + getInfo: (loadCurrentValues: boolean) => Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + }>; + + // Deprecated: + onLicense: typeof onLicense; + // Deprecated: + getMaxActiveUsers: typeof getMaxActiveUsers; + // Deprecated: + getAppsConfig: typeof getAppsConfig; + // Deprecated: + getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; +} + +export class LicenseImp extends LicenseManager implements License { + validateFormat = validateFormat; + + hasModule = hasModule; + + getModules = getModules; + + getTags = getTags; + + overwriteClassOnLicense = overwriteClassOnLicense; + + public setLicenseLimitCounter = setLicenseLimitCounter; + + getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; + + public async isLimitReached(action: T, context?: Partial>): Promise { + return this.shouldPreventAction(action, 0, context); + } + + onValidFeature = onValidFeature; + + onInvalidFeature = onInvalidFeature; + + onToggledFeature = onToggledFeature; + + onModule = onModule; + + onValidateLicense = onValidateLicense; + + onInvalidateLicense = onInvalidateLicense; + + onLimitReached = onLimitReached; + + onBehaviorTriggered = onBehaviorTriggered; + + // Deprecated: + onLicense = onLicense; + + // Deprecated: + getMaxActiveUsers = getMaxActiveUsers; + + // Deprecated: + getAppsConfig = getAppsConfig; + + // Deprecated: + getUnmodifiedLicenseAndModules = getUnmodifiedLicenseAndModules; +} + +const license = new LicenseImp(); + +export { license as License }; diff --git a/ee/packages/license/src/isItemAllowed.ts b/ee/packages/license/src/isItemAllowed.ts new file mode 100644 index 0000000000000..16787cdf9c4d7 --- /dev/null +++ b/ee/packages/license/src/isItemAllowed.ts @@ -0,0 +1,12 @@ +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseBehavior } from './definition/LicenseBehavior'; +import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; + +const isItemAllowed = (item: T, allowList?: T[]): boolean => { + return !allowList || allowList.includes(item); +}; + +export const isLimitAllowed = (item: LicenseLimitKind, options: LicenseValidationOptions): boolean => isItemAllowed(item, options.limits); + +export const isBehaviorAllowed = (item: LicenseBehavior, options: LicenseValidationOptions): boolean => + isItemAllowed(item, options.behaviors) && (options.isNewLicense || item !== 'prevent_installation'); diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts new file mode 100644 index 0000000000000..989be7b69ae10 --- /dev/null +++ b/ee/packages/license/src/license.spec.ts @@ -0,0 +1,195 @@ +import { MockedLicenseBuilder, getReadyLicenseManager } from '../__tests__/MockedLicenseBuilder'; + +it('should not prevent if there is no license', async () => { + const license = await getReadyLicenseManager(); + const result = await license.shouldPreventAction('activeUsers'); + expect(result).toBe(false); +}); + +it('should not prevent if the counter is under the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); +}); + +it('should prevent if the counter is equal or over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); +}); + +describe('Validate License Limits', () => { + describe('prevent_action behavior', () => { + describe('during the licensing apply', () => { + it('should not trigger the event even if the counter is over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const preventActionCallback = jest.fn(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback); + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + expect(preventActionCallback).toHaveBeenCalledTimes(0); + }); + }); + }); + describe('fair usage behavior', () => { + it('should change the flag to true if the counter is equal or over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const fairUsageCallback = jest.fn(); + const preventActionCallback = jest.fn(); + + licenseManager.onBehaviorTriggered('start_fair_policy', fairUsageCallback); + licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + { + max: 10, + behavior: 'start_fair_policy', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); + expect(fairUsageCallback).toHaveBeenCalledTimes(0); + expect(preventActionCallback).toHaveBeenCalledTimes(0); + + preventActionCallback.mockClear(); + fairUsageCallback.mockClear(); + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + expect(fairUsageCallback).toHaveBeenCalledTimes(0); + expect(preventActionCallback).toHaveBeenCalledTimes(1); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + preventActionCallback.mockClear(); + fairUsageCallback.mockClear(); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + expect(preventActionCallback).toHaveBeenCalledTimes(4); + expect(fairUsageCallback).toHaveBeenCalledTimes(4); + }); + }); + + describe('invalidate_license behavior', () => { + it('should invalidate the license if the counter is over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const invalidateCallback = jest.fn(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + { + max: 10, + behavior: 'invalidate_license', + }, + ]); + + licenseManager.on('invalidate', invalidateCallback); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); + await expect(licenseManager.hasValidLicense()).toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); + await expect(licenseManager.hasValidLicense()).toBe(true); + + await licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + await expect(licenseManager.hasValidLicense()).toBe(true); + expect(invalidateCallback).toHaveBeenCalledTimes(0); + + await licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + await expect(licenseManager.hasValidLicense()).toBe(false); + expect(invalidateCallback).toHaveBeenCalledTimes(1); + }); + }); + + describe('prevent action for future limits', () => { + it('should prevent if the counter plus the extra value is equal or over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + const fairUsageCallback = jest.fn(); + const preventActionCallback = jest.fn(); + + licenseManager.onBehaviorTriggered('start_fair_policy', fairUsageCallback); + licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); + expect(fairUsageCallback).toHaveBeenCalledTimes(0); + expect(preventActionCallback).toHaveBeenCalledTimes(0); + + for await (const extraCount of [1, 2, 3, 4, 5]) { + await expect(licenseManager.shouldPreventAction('activeUsers', extraCount)).resolves.toBe(false); + expect(fairUsageCallback).toHaveBeenCalledTimes(0); + expect(preventActionCallback).toHaveBeenCalledTimes(0); + } + + /** + * if we are testing the current count 10 should prevent the action, if we are testing the future count 10 should not prevent the action but 11 + */ + + await expect(licenseManager.shouldPreventAction('activeUsers', 6)).resolves.toBe(true); + expect(fairUsageCallback).toHaveBeenCalledTimes(0); + expect(preventActionCallback).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts new file mode 100644 index 0000000000000..8449d4136810b --- /dev/null +++ b/ee/packages/license/src/license.ts @@ -0,0 +1,330 @@ +import { Emitter } from '@rocket.chat/emitter'; + +import { type ILicenseTag } from './definition/ILicenseTag'; +import type { ILicenseV2 } from './definition/ILicenseV2'; +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseModule } from './definition/LicenseModule'; +import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; +import type { LimitContext } from './definition/LimitContext'; +import type { LicenseEvents } from './definition/events'; +import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; +import { InvalidLicenseError } from './errors/InvalidLicenseError'; +import { NotReadyForValidation } from './errors/NotReadyForValidation'; +import { behaviorTriggered, licenseInvalidated, licenseValidated } from './events/emitter'; +import { logger } from './logger'; +import { getModules, invalidateAll, replaceModules } from './modules'; +import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import { showLicense } from './showLicense'; +import { replaceTags } from './tags'; +import { decrypt } from './token'; +import { convertToV3 } from './v2/convertToV3'; +import { filterBehaviorsResult } from './validation/filterBehaviorsResult'; +import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; +import { getModulesToDisable } from './validation/getModulesToDisable'; +import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; +import { isReadyForValidation } from './validation/isReadyForValidation'; +import { runValidation } from './validation/runValidation'; +import { validateFormat } from './validation/validateFormat'; + +const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts']; + +export class LicenseManager extends Emitter { + dataCounters = new Map) => Promise>(); + + pendingLicense = ''; + + tags = new Set(); + + modules = new Set(); + + private workspaceUrl: string | undefined; + + private _license: ILicenseV3 | undefined; + + private _unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; + + private _valid: boolean | undefined; + + private _lockedLicense: string | undefined; + + constructor() { + super(); + + this.on('validate', () => showLicense.call(this, this._license, this._valid)); + } + + public get license(): ILicenseV3 | undefined { + return this._license; + } + + public get unmodifiedLicense(): ILicenseV2 | ILicenseV3 | undefined { + return this._unmodifiedLicense; + } + + public get valid(): boolean | undefined { + return this._valid; + } + + public get encryptedLicense(): string | undefined { + if (!this.hasValidLicense()) { + return undefined; + } + + return this._lockedLicense; + } + + public async setWorkspaceUrl(url: string) { + this.workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); + + if (hasPendingLicense.call(this)) { + await applyPendingLicense.call(this); + } + } + + public getWorkspaceUrl() { + return this.workspaceUrl; + } + + public async revalidateLicense(options: Omit = {}): Promise { + if (!this.hasValidLicense()) { + return; + } + + try { + await this.validateLicense({ ...options, isNewLicense: false }); + } catch (e) { + if (e instanceof InvalidLicenseError) { + this.invalidateLicense(); + } + } + } + + private clearLicenseData(): void { + this._license = undefined; + this._unmodifiedLicense = undefined; + this._valid = false; + this._lockedLicense = undefined; + clearPendingLicense.call(this); + } + + private invalidateLicense(): void { + this._valid = false; + licenseInvalidated.call(this); + invalidateAll.call(this); + } + + private async setLicenseV3( + newLicense: ILicenseV3, + encryptedLicense: string, + originalLicense?: ILicenseV2 | ILicenseV3, + isNewLicense?: boolean, + ): Promise { + const hadValidLicense = this.hasValidLicense(); + this.clearLicenseData(); + + try { + this._unmodifiedLicense = originalLicense || newLicense; + this._license = newLicense; + + this._lockedLicense = encryptedLicense; + + await this.validateLicense({ isNewLicense }); + } catch (e) { + if (e instanceof InvalidLicenseError) { + if (hadValidLicense) { + this.invalidateLicense(); + } + } + } + } + + private async setLicenseV2(newLicense: ILicenseV2, encryptedLicense: string, isNewLicense?: boolean): Promise { + return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense, isNewLicense); + } + + private isLicenseDuplicated(encryptedLicense: string): boolean { + return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); + } + + private async validateLicense(options: LicenseValidationOptions = {}): Promise { + if (!this._license) { + throw new InvalidLicenseError(); + } + + if (!isReadyForValidation.call(this)) { + throw new NotReadyForValidation(); + } + + const validationResult = await runValidation.call(this, this._license, { + behaviors: ['invalidate_license', 'start_fair_policy', 'prevent_installation', 'disable_modules'], + ...options, + }); + + if (isBehaviorsInResult(validationResult, ['invalidate_license', 'prevent_installation'])) { + throw new InvalidLicenseError(); + } + + const shouldLogModules = !this._valid || options.isNewLicense; + + this._valid = true; + + if (this._license.information.tags) { + replaceTags.call(this, this._license.information.tags); + } + + const disabledModules = getModulesToDisable(validationResult); + const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + const modulesChanged = replaceModules.call( + this, + modulesToEnable.map(({ module }) => module), + ); + + if (shouldLogModules || modulesChanged) { + logger.log({ msg: 'License validated', modules: modulesToEnable }); + } + + if (!options.isNewLicense) { + this.triggerBehaviorEvents(validationResult); + } + + licenseValidated.call(this); + } + + public async setLicense(encryptedLicense: string, isNewLicense = true): Promise { + if (!(await validateFormat(encryptedLicense))) { + throw new InvalidLicenseError(); + } + + if (this.isLicenseDuplicated(encryptedLicense)) { + // If there is a pending license but the user is trying to revert to the license that is currently active + if (hasPendingLicense.call(this) && !isPendingLicense.call(this, encryptedLicense)) { + // simply remove the pending license + clearPendingLicense.call(this); + throw new Error('Invalid license 1'); + } + + throw new DuplicatedLicenseError(); + } + + if (!isReadyForValidation.call(this)) { + // If we can't validate the license data yet, but is a valid license string, store it to validate when we can + setPendingLicense.call(this, encryptedLicense); + throw new NotReadyForValidation(); + } + + logger.info('New Enterprise License'); + try { + const decrypted = JSON.parse(await decrypt(encryptedLicense)); + + logger.debug({ msg: 'license', decrypted }); + + if (!encryptedLicense.startsWith('RCV3_')) { + await this.setLicenseV2(decrypted, encryptedLicense, isNewLicense); + return true; + } + await this.setLicenseV3(decrypted, encryptedLicense, decrypted, isNewLicense); + + return true; + } catch (e) { + logger.error('Invalid license'); + + logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + + throw new InvalidLicenseError(); + } + } + + private triggerBehaviorEvents(validationResult: BehaviorWithContext[]): void { + for (const { ...options } of validationResult) { + behaviorTriggered.call(this, { ...options }); + } + } + + public hasValidLicense(): boolean { + return Boolean(this.getLicense()); + } + + public getLicense(): ILicenseV3 | undefined { + if (this._valid && this._license) { + return this._license; + } + } + + public async shouldPreventAction( + action: T, + extraCount = 0, + context: Partial> = {}, + { suppressLog }: Pick = {}, + ): Promise { + const license = this.getLicense(); + if (!license) { + return false; + } + + const options: LicenseValidationOptions = { + ...(extraCount && { behaviors: ['prevent_action'] }), + isNewLicense: false, + suppressLog: !!suppressLog, + context: { + [action]: { + extraCount, + ...context, + }, + }, + }; + + const validationResult = await runValidation.call(this, license, options); + + // extra values should not call events since they are not actually reaching the limit just checking if they would + if (extraCount) { + return isBehaviorsInResult(validationResult, ['prevent_action']); + } + + if (isBehaviorsInResult(validationResult, ['invalidate_license', 'disable_modules', 'start_fair_policy'])) { + await this.revalidateLicense(); + } + + this.triggerBehaviorEvents(filterBehaviorsResult(validationResult, ['prevent_action'])); + + return isBehaviorsInResult(validationResult, ['prevent_action']); + } + + public async getInfo(loadCurrentValues = false): Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + }> { + const activeModules = getModules.call(this); + const license = this.getLicense(); + + // Get all limits present in the license and their current value + const limits = ( + (license && + (await Promise.all( + globalLimitKinds + .map((limitKey) => ({ + limitKey, + max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), + })) + .filter(({ max }) => max >= 0 && max < Infinity) + .map(async ({ max, limitKey }) => { + return { + [limitKey as LicenseLimitKind]: { + ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit.call(this, limitKey as LicenseLimitKind) } : {}), + max, + }, + }; + }), + ))) || + [] + ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); + + return { + license, + activeModules, + limits: limits as Record, + }; + } +} diff --git a/ee/packages/license/src/logger.ts b/ee/packages/license/src/logger.ts new file mode 100644 index 0000000000000..120b08691c6c4 --- /dev/null +++ b/ee/packages/license/src/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const logger = new Logger('License'); diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts new file mode 100644 index 0000000000000..6931fb7a6a5d2 --- /dev/null +++ b/ee/packages/license/src/modules.ts @@ -0,0 +1,55 @@ +import type { LicenseModule } from './definition/LicenseModule'; +import { moduleRemoved, moduleValidated } from './events/emitter'; +import type { LicenseManager } from './license'; + +export function notifyValidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { + licenseModules.forEach((module) => { + this.modules.add(module); + moduleValidated.call(this, module); + }); +} + +export function notifyInvalidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { + licenseModules.forEach((module) => { + moduleRemoved.call(this, module); + this.modules.delete(module); + }); +} + +export function invalidateAll(this: LicenseManager) { + notifyInvalidatedModules.call(this, [...this.modules]); + this.modules.clear(); +} + +export function getModules(this: LicenseManager) { + return [...this.modules]; +} + +export function hasModule(this: LicenseManager, module: LicenseModule) { + return this.modules.has(module); +} + +export function replaceModules(this: LicenseManager, newModules: LicenseModule[]): boolean { + let anyChange = false; + for (const moduleName of newModules) { + if (this.modules.has(moduleName)) { + continue; + } + + this.modules.add(moduleName); + moduleValidated.call(this, moduleName); + anyChange = true; + } + + for (const moduleName of this.modules) { + if (newModules.includes(moduleName)) { + continue; + } + + moduleRemoved.call(this, moduleName); + this.modules.delete(moduleName); + anyChange = true; + } + + return anyChange; +} diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts new file mode 100644 index 0000000000000..8dd82dcd7774d --- /dev/null +++ b/ee/packages/license/src/pendingLicense.ts @@ -0,0 +1,32 @@ +import type { LicenseManager } from './license'; +import { logger } from './logger'; + +export function setPendingLicense(this: LicenseManager, encryptedLicense: string) { + this.pendingLicense = encryptedLicense; + if (this.pendingLicense) { + logger.info('Storing license as pending validation.'); + } +} + +export async function applyPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { + logger.info('Applying pending license.'); + return this.setLicense(this.pendingLicense); + } +} + +export function hasPendingLicense(this: LicenseManager) { + return Boolean(this.pendingLicense); +} + +export function isPendingLicense(this: LicenseManager, encryptedLicense: string) { + return !!this.pendingLicense && this.pendingLicense === encryptedLicense; +} + +export function clearPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { + logger.info('Removing pending license.'); + } + + this.pendingLicense = ''; +} diff --git a/ee/packages/license/src/showLicense.ts b/ee/packages/license/src/showLicense.ts new file mode 100644 index 0000000000000..3dda60a43b765 --- /dev/null +++ b/ee/packages/license/src/showLicense.ts @@ -0,0 +1,27 @@ +import type { ILicenseV3 } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; +import { getModules } from './modules'; + +export function showLicense(this: LicenseManager, license: ILicenseV3 | undefined, valid: boolean | undefined) { + if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { + return; + } + + if (!license || !valid) { + return; + } + + const { + validation: { serverUrls, validPeriods }, + limits, + } = license; + + const modules = getModules.call(this); + + console.log('---- License enabled ----'); + console.log(' url ->', JSON.stringify(serverUrls)); + console.log(' periods ->', JSON.stringify(validPeriods)); + console.log(' limits ->', JSON.stringify(limits)); + console.log(' modules ->', modules.join(', ')); + console.log('-------------------------'); +} diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts new file mode 100644 index 0000000000000..33434cae116d5 --- /dev/null +++ b/ee/packages/license/src/tags.ts @@ -0,0 +1,24 @@ +import type { ILicenseTag } from './definition/ILicenseTag'; +import { type LicenseManager } from './license'; + +export function addTag(this: LicenseManager, tag: ILicenseTag) { + // make sure to not add duplicated tag names + for (const addedTag of this.tags) { + if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { + return; + } + } + + this.tags.add(tag); +} + +export function replaceTags(this: LicenseManager, newTags: ILicenseTag[]) { + this.tags.clear(); + for (const tag of newTags) { + addTag.call(this, tag); + } +} + +export function getTags(this: LicenseManager) { + return [...this.tags]; +} diff --git a/ee/packages/license/src/token.ts b/ee/packages/license/src/token.ts new file mode 100644 index 0000000000000..2a9836a483039 --- /dev/null +++ b/ee/packages/license/src/token.ts @@ -0,0 +1,55 @@ +import crypto from 'crypto'; + +import { verify, sign, getPairs } from '@rocket.chat/jwt'; + +import type { ILicenseV3 } from './definition/ILicenseV3'; + +const PUBLIC_LICENSE_KEY_V2 = + 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; + +const PUBLIC_LICENSE_KEY_V3 = process.env.PUBLIC_LICENSE_KEY_V3 || PUBLIC_LICENSE_KEY_V2; + +let TEST_KEYS: [string, string] | undefined = undefined; + +export async function decrypt(encrypted: string): Promise { + if (process.env.NODE_ENV === 'test') { + if (encrypted.startsWith('RCV3_')) { + const jwt = encrypted.substring(5); + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + if (!TEST_KEYS) { + throw new Error('Missing PUBLIC_LICENSE_KEY_V3'); + } + + const [spki] = TEST_KEYS; + + const [payload] = await verify(jwt, spki); + return JSON.stringify(payload); + } + } + + // handle V3 + if (encrypted.startsWith('RCV3_')) { + const jwt = encrypted.substring(5); + const [payload] = await verify(jwt, PUBLIC_LICENSE_KEY_V3); + + return JSON.stringify(payload); + } + + const decrypted = crypto.publicDecrypt(Buffer.from(PUBLIC_LICENSE_KEY_V2, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); + + return decrypted.toString('utf-8'); +} + +export async function encrypt(license: ILicenseV3): Promise { + if (process.env.NODE_ENV !== 'test') { + throw new Error('This function should only be used in tests'); + } + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + const [, pkcs8] = TEST_KEYS; + + return `RCV3_${await sign(license, pkcs8)}`; +} diff --git a/apps/meteor/ee/app/license/server/bundles.ts b/ee/packages/license/src/v2/bundles.ts similarity index 97% rename from apps/meteor/ee/app/license/server/bundles.ts rename to ee/packages/license/src/v2/bundles.ts index 507283b3e60fd..70f9d7b5a653f 100644 --- a/apps/meteor/ee/app/license/server/bundles.ts +++ b/ee/packages/license/src/v2/bundles.ts @@ -8,7 +8,6 @@ export type BundleFeature = | 'engagement-dashboard' | 'push-privacy' | 'scalability' - | 'teams-mention' | 'saml-enterprise' | 'device-management' | 'oauth-enterprise' @@ -32,7 +31,6 @@ const bundles: IBundle = { 'engagement-dashboard', 'push-privacy', 'scalability', - 'teams-mention', 'saml-enterprise', 'oauth-enterprise', 'device-management', diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts new file mode 100644 index 0000000000000..10681cf04b47b --- /dev/null +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -0,0 +1,114 @@ +/** + * FromV2ToV3 + * Transform a License V2 into a V3 representation. + */ + +import type { ILicenseV2 } from '../definition/ILicenseV2'; +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { LicenseModule } from '../definition/LicenseModule'; +import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; +import { getTagColor } from './getTagColor'; + +export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { + return { + version: '3.0', + information: { + autoRenew: false, + visualExpiration: new Date(Date.parse(v2.meta?.trialEnd || v2.expiry)).toISOString(), + trial: v2.meta?.trial || false, + offline: false, + createdAt: new Date().toISOString(), + grantedBy: { + method: 'manual', + seller: 'V2', + }, + // if no tag present, it means it is an old license, so try check for bundles and use them as tags + tags: v2.tag + ? [v2.tag] + : [ + ...(v2.modules.filter(isBundle).map(getBundleFromModule).filter(Boolean) as string[]).map((tag) => ({ + name: tag, + color: getTagColor(tag), + })), + ], + }, + validation: { + serverUrls: [ + { + value: v2.url, + type: 'regex', + }, + ], + validPeriods: [ + { + validUntil: new Date(Date.parse(v2.expiry)).toISOString(), + invalidBehavior: 'invalidate_license', + }, + ], + statisticsReport: { + required: true, + }, + }, + grantedModules: [ + ...new Set( + v2.modules + .map((licenseModule) => (isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule])) + .reduce((prev, curr) => [...prev, ...curr], []) + .map((licenseModule) => ({ module: licenseModule as LicenseModule })), + ), + ], + limits: { + ...(v2.maxActiveUsers + ? { + activeUsers: [ + { + max: v2.maxActiveUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxGuestUsers + ? { + guestUsers: [ + { + max: v2.maxGuestUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxRoomsPerGuest + ? { + roomsPerGuest: [ + { + max: v2.maxRoomsPerGuest, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxPrivateApps + ? { + privateApps: [ + { + max: v2.apps.maxPrivateApps, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxMarketplaceApps + ? { + marketplaceApps: [ + { + max: v2.apps.maxMarketplaceApps, + behavior: 'prevent_action', + }, + ], + } + : {}), + }, + cloudMeta: v2.meta, + }; +}; diff --git a/apps/meteor/ee/app/license/server/getTagColor.ts b/ee/packages/license/src/v2/getTagColor.ts similarity index 100% rename from apps/meteor/ee/app/license/server/getTagColor.ts rename to ee/packages/license/src/v2/getTagColor.ts diff --git a/ee/packages/license/src/validation/filterBehaviorsResult.ts b/ee/packages/license/src/validation/filterBehaviorsResult.ts new file mode 100644 index 0000000000000..e51dbac20a535 --- /dev/null +++ b/ee/packages/license/src/validation/filterBehaviorsResult.ts @@ -0,0 +1,4 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; + +export const filterBehaviorsResult = (result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]) => + result.filter(({ behavior }) => expectedBehaviors.includes(behavior)); diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts new file mode 100644 index 0000000000000..8f9c6ed4034e6 --- /dev/null +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -0,0 +1,37 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LimitContext } from '../definition/LimitContext'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { applyPendingLicense, hasPendingLicense } from '../pendingLicense'; + +export function setLicenseLimitCounter( + this: LicenseManager, + limitKey: T, + fn: (context?: LimitContext) => Promise | number, +) { + this.dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); + + if (hasPendingLicense.call(this) && hasAllDataCounters.call(this)) { + void applyPendingLicense.call(this); + } +} + +export async function getCurrentValueForLicenseLimit( + this: LicenseManager, + limitKey: T, + context?: Partial>, +): Promise { + const counterFn = this.dataCounters.get(limitKey); + if (!counterFn) { + logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); + throw new Error('Unable to validate license limit due to missing data counter.'); + } + + return counterFn(context as LimitContext | undefined); +} + +export function hasAllDataCounters(this: LicenseManager) { + return ( + ['activeUsers', 'guestUsers', 'roomsPerGuest', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[] + ).every((limitKey) => this.dataCounters.has(limitKey)); +} diff --git a/ee/packages/license/src/validation/getModulesToDisable.ts b/ee/packages/license/src/validation/getModulesToDisable.ts new file mode 100644 index 0000000000000..d42426e8af268 --- /dev/null +++ b/ee/packages/license/src/validation/getModulesToDisable.ts @@ -0,0 +1,15 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseModule } from '../definition/LicenseModule'; + +const filterValidationResult = (result: BehaviorWithContext[], expectedBehavior: LicenseBehavior) => + result.filter(({ behavior }) => behavior === expectedBehavior) as BehaviorWithContext[]; + +export const getModulesToDisable = (validationResult: BehaviorWithContext[]): LicenseModule[] => { + return [ + ...new Set([ + ...filterValidationResult(validationResult, 'disable_modules') + .map(({ modules }) => modules || []) + .flat(), + ]), + ]; +}; diff --git a/ee/packages/license/src/validation/getResultingBehavior.ts b/ee/packages/license/src/validation/getResultingBehavior.ts new file mode 100644 index 0000000000000..22ca02bfd220f --- /dev/null +++ b/ee/packages/license/src/validation/getResultingBehavior.ts @@ -0,0 +1,28 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseLimit } from '../definition/LicenseLimit'; +import type { LicensePeriod } from '../definition/LicensePeriod'; + +export const getResultingBehavior = ( + data: LicenseLimit | LicensePeriod | Partial>, + { reason, limit }: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }, +): BehaviorWithContext => { + const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; + + switch (behavior) { + case 'disable_modules': + return { + behavior, + modules: ('modules' in data && data.modules) || [], + reason, + limit, + }; + + default: + return { + behavior, + reason, + limit, + } as BehaviorWithContext; + } +}; diff --git a/ee/packages/license/src/validation/isBehaviorsInResult.ts b/ee/packages/license/src/validation/isBehaviorsInResult.ts new file mode 100644 index 0000000000000..7e6ed89db8ece --- /dev/null +++ b/ee/packages/license/src/validation/isBehaviorsInResult.ts @@ -0,0 +1,4 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; + +export const isBehaviorsInResult = (result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]) => + result.some(({ behavior }) => expectedBehaviors.includes(behavior)); diff --git a/ee/packages/license/src/validation/isReadyForValidation.ts b/ee/packages/license/src/validation/isReadyForValidation.ts new file mode 100644 index 0000000000000..aa763bf7f353d --- /dev/null +++ b/ee/packages/license/src/validation/isReadyForValidation.ts @@ -0,0 +1,7 @@ +import type { LicenseManager } from '../license'; +import { hasAllDataCounters } from './getCurrentValueForLicenseLimit'; + +// Can only validate licenses once the workspace URL and the data counter functions are set +export function isReadyForValidation(this: LicenseManager) { + return Boolean(this.getWorkspaceUrl() && hasAllDataCounters.call(this)); +} diff --git a/ee/packages/license/src/validation/runValidation.spec.ts b/ee/packages/license/src/validation/runValidation.spec.ts new file mode 100644 index 0000000000000..523090acd63a0 --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.spec.ts @@ -0,0 +1,38 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; +import { runValidation } from './runValidation'; + +describe('Validation behaviors', () => { + it('should return a behavior if the license period is invalid', async () => { + const licenseManager = await getReadyLicenseManager(); + + // two days ago + const validFrom = new Date(new Date().setDate(new Date().getDate() - 2)); + // one day ago + const validUntil = new Date(new Date().setDate(new Date().getDate() - 1)); + + const license = await new MockedLicenseBuilder().resetValidPeriods().withValidPeriod({ + validFrom: validFrom.toISOString(), + validUntil: validUntil.toISOString(), + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + }); + + await expect( + runValidation.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).resolves.toStrictEqual([ + { + behavior: 'disable_modules', + limit: undefined, + modules: ['livechat-enterprise'], + reason: 'period', + }, + ]); + }); +}); diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts new file mode 100644 index 0000000000000..922b4c49162ee --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.ts @@ -0,0 +1,19 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import type { LicenseManager } from '../license'; +import { validateLicenseLimits } from './validateLicenseLimits'; +import { validateLicensePeriods } from './validateLicensePeriods'; +import { validateLicenseUrl } from './validateLicenseUrl'; + +export async function runValidation( + this: LicenseManager, + license: ILicenseV3, + options: LicenseValidationOptions, +): Promise { + return [ + ...validateLicenseUrl.call(this, license, options), + ...validateLicensePeriods(license, options), + ...(await validateLicenseLimits.call(this, license, options)), + ]; +} diff --git a/ee/packages/license/src/validation/validateFormat.ts b/ee/packages/license/src/validation/validateFormat.ts new file mode 100644 index 0000000000000..a8c2488cd9fc1 --- /dev/null +++ b/ee/packages/license/src/validation/validateFormat.ts @@ -0,0 +1,16 @@ +import { InvalidLicenseError } from '../errors/InvalidLicenseError'; +import { decrypt } from '../token'; + +export const validateFormat = async (encryptedLicense: string): Promise => { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + throw new InvalidLicenseError('Empty license'); + } + + try { + await decrypt(encryptedLicense); + } catch (e) { + throw new InvalidLicenseError(); + } + + return true; +}; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts new file mode 100644 index 0000000000000..f321252ba573c --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -0,0 +1,60 @@ +import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isLimitAllowed, isBehaviorAllowed } from '../isItemAllowed'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; +import { getResultingBehavior } from './getResultingBehavior'; + +export async function validateLicenseLimits( + this: LicenseManager, + license: ILicenseV3, + options: LicenseValidationOptions, +): Promise { + const { limits } = license; + + const limitKeys = (Object.keys(limits) as LicenseLimitKind[]).filter((limit) => isLimitAllowed(limit, options)); + return ( + await Promise.all( + limitKeys.map(async (limitKey) => { + // Filter the limit list before running any query in the database so we don't end up loading some value we won't use. + const limitList = limits[limitKey]?.filter(({ behavior, max }) => max >= 0 && isBehaviorAllowed(behavior, options)); + if (!limitList?.length) { + return []; + } + + const extraCount = options.context?.[limitKey]?.extraCount ?? 0; + const currentValue = (await getCurrentValueForLicenseLimit.call(this, limitKey, options.context?.[limitKey])) + extraCount; + + return limitList + .filter(({ max, behavior }) => { + switch (behavior) { + case 'invalidate_license': + case 'prevent_installation': + case 'disable_modules': + case 'start_fair_policy': + default: + return currentValue > max; + case 'prevent_action': + /** + * if we are validating the current count the limit should be equal or over the max, if we are validating the future count the limit should be over the max + */ + + return extraCount ? currentValue > max : currentValue >= max; + } + }) + .map((limit) => { + if (!options.suppressLog) { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + } + return getResultingBehavior(limit, { reason: 'limit', limit: limitKey }); + }); + }), + ) + ).flat(); +} diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts new file mode 100644 index 0000000000000..fb27f72d0a8ed --- /dev/null +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -0,0 +1,42 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { Timestamp } from '../definition/LicensePeriod'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isBehaviorAllowed } from '../isItemAllowed'; +import { logger } from '../logger'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const isPeriodInvalid = (from: Timestamp | undefined, until: Timestamp | undefined) => { + const now = new Date(); + + if (from && now < new Date(from)) { + return true; + } + + if (until && now > new Date(until)) { + return true; + } + + return false; +}; + +export const validateLicensePeriods = (license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] => { + const { + validation: { validPeriods }, + } = license; + + return validPeriods + .filter( + ({ validFrom, validUntil, invalidBehavior }) => isBehaviorAllowed(invalidBehavior, options) && isPeriodInvalid(validFrom, validUntil), + ) + .map((period) => { + if (!options.suppressLog) { + logger.error({ + msg: 'Period validation failed', + period, + }); + } + + return getResultingBehavior(period, { reason: 'period' }); + }); +}; diff --git a/ee/packages/license/src/validation/validateLicenseUrl.spec.ts b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts new file mode 100644 index 0000000000000..9047876f8fbcc --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts @@ -0,0 +1,130 @@ +/** + * @jest-environment node + */ + +import crypto from 'crypto'; + +import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; +import { validateLicenseUrl } from './validateLicenseUrl'; + +describe('Url Validation', () => { + describe('url method', () => { + it('should return a behavior if the license url is invalid', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:3001', + type: 'url', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + + it('should return an empty array if the license url is valid', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:3000', + type: 'url', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); + + describe('regex method', () => { + it('should return a behavior if the license does not match the regex', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'unstable.rocket.*', + type: 'regex', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + + it('should return an empty array if the license matches the regex', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:300*', + type: 'regex', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); + + describe('hash method', () => { + it('should return a behavior if the license does not match the hash', async () => { + const licenseManager = await getReadyLicenseManager(); + + const hash = crypto.createHash('sha256').update('localhost:3001').digest('hex'); + const license = await new MockedLicenseBuilder().withServerUrls({ + value: hash, + type: 'hash', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + it('should return an empty array if the license matches the hash', async () => { + const licenseManager = await getReadyLicenseManager(); + + const hash = crypto.createHash('sha256').update('localhost:3000').digest('hex'); + const license = await new MockedLicenseBuilder().withServerUrls({ + value: hash, + type: 'hash', + }); + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); +}); diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts new file mode 100644 index 0000000000000..416b107511cb4 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -0,0 +1,68 @@ +import crypto from 'crypto'; + +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isBehaviorAllowed } from '../isItemAllowed'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { getResultingBehavior } from './getResultingBehavior'; + +const validateRegex = (licenseURL: string, url: string) => { + licenseURL = licenseURL + .replace(/\./g, '\\.') // convert dots to literal + .replace(/\*/g, '.*'); // convert * to .* + const regex = new RegExp(`^${licenseURL}$`, 'i'); + + return !!regex.exec(url); +}; + +const validateUrl = (licenseURL: string, url: string) => { + return licenseURL.toLowerCase() === url.toLowerCase(); +}; + +const validateHash = (licenseURL: string, url: string) => { + const value = crypto.createHash('sha256').update(url).digest('hex'); + return licenseURL === value; +}; + +export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] { + if (!isBehaviorAllowed('invalidate_license', options)) { + return []; + } + + const { + validation: { serverUrls }, + } = license; + + const workspaceUrl = this.getWorkspaceUrl(); + + if (!workspaceUrl) { + logger.error('Unable to validate license URL without knowing the workspace URL.'); + return [getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' })]; + } + + return serverUrls + .filter((url) => { + switch (url.type) { + case 'regex': + return !validateRegex(url.value, workspaceUrl); + case 'hash': + return !validateHash(url.value, workspaceUrl); + case 'url': + return !validateUrl(url.value, workspaceUrl); + } + + return false; + }) + .map((url) => { + if (!options.suppressLog) { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + } + return getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' }); + }); +} diff --git a/ee/packages/license/tsconfig.json b/ee/packages/license/tsconfig.json new file mode 100644 index 0000000000000..539d1c0af1b81 --- /dev/null +++ b/ee/packages/license/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index c141b4205af53..6e4556b12ef03 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,168 @@ # @rocket.chat/omnichannel-services +## 0.0.15 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/model-typings@0.1.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/tools@0.1.0 + - @rocket.chat/pdf-worker@0.0.15 + - @rocket.chat/models@0.0.15 + +## 0.0.15-rc.5 + +### Patch Changes + +- 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/pdf-worker@0.0.15-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/model-typings@0.1.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.0.14-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/pdf-worker@0.0.14-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/model-typings@0.1.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.0.14-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/pdf-worker@0.0.14-rc.3 +- @rocket.chat/core-services@0.2.0-rc.3 +- @rocket.chat/model-typings@0.1.0-rc.3 +- @rocket.chat/models@0.0.14-rc.3 + +## 0.0.14-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/pdf-worker@0.0.14-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/model-typings@0.1.0-rc.2 +- @rocket.chat/models@0.0.14-rc.2 + +## 0.0.14-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/pdf-worker@0.0.14-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/model-typings@0.1.0-rc.1 +- @rocket.chat/models@0.0.14-rc.1 + +## 0.0.14-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [8a59855fcf] +- Updated dependencies [5cee21468e] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [aaefe865a7] +- Updated dependencies [357a3a50fa] +- Updated dependencies [f556518fa1] +- Updated dependencies [ead7c7bef2] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/model-typings@0.1.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.0.11-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.0.13 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/pdf-worker@0.0.13 +- @rocket.chat/core-services@0.1.7 +- @rocket.chat/model-typings@0.0.13 +- @rocket.chat/models@0.0.13 + +## 0.0.12 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/pdf-worker@0.0.12 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/model-typings@0.0.12 +- @rocket.chat/models@0.0.12 + +## 0.0.11 + +### Patch Changes + +- Updated dependencies [92d25b9c7a] + - @rocket.chat/model-typings@0.0.11 + - @rocket.chat/models@0.0.11 + - @rocket.chat/core-services@0.1.5 + - @rocket.chat/core-typings@6.3.5 + - @rocket.chat/rest-typings@6.3.5 + - @rocket.chat/pdf-worker@0.0.11 + +## 0.0.10 + +### Patch Changes + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + - @rocket.chat/pdf-worker@0.0.10 + ## 0.0.9 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 27c04e99e7ea8..e5875c054eef1 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.0.9", + "version": "0.0.15", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", @@ -29,7 +29,7 @@ "mem": "^8.1.1", "moment-timezone": "^0.5.43", "mongo-message-queue": "^1.0.0", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "pino": "^8.15.0" }, "scripts": { diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 802a6e15d0eb6..0e135d5ed2639 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -78,7 +78,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT async started(): Promise { try { - this.shouldWork = await licenseService.hasLicense('scalability'); + this.shouldWork = await licenseService.hasModule('scalability'); } catch (e: unknown) { // ignore } @@ -222,7 +222,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } let file = message.files?.map((v) => ({ _id: v._id, name: v.name })).find((file) => file.name === attachment.title); if (!file) { - this.log.debug(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); // For some reason, when an image is uploaded from clipboard, it doesn't have a file :( // So, we'll try to get the FILE_ID from the `title_link` prop which has the format `/file-upload/FILE_ID/FILE_NAME` using a regex const fileId = attachment.title_link?.match(/\/file-upload\/(.*)\/.*/)?.[1]; @@ -236,7 +236,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } if (!file) { - this.log.error(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); // ignore attachments without file files.push({ name: attachment.title, buffer: null }); continue; @@ -304,7 +304,8 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT const messages = await this.getMessagesFromRoom({ rid: room._id }); const visitor = - room.v && (await LivechatVisitors.findOneById(room.v._id, { projection: { _id: 1, name: 1, username: 1, visitorEmails: 1 } })); + room.v && + (await LivechatVisitors.findOneEnabledById(room.v._id, { projection: { _id: 1, name: 1, username: 1, visitorEmails: 1 } })); const agent = room.servedBy && (await Users.findOneAgentById(room.servedBy._id, { projection: { _id: 1, name: 1, username: 1, utcOffset: 1 } })); diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts index 141cb937f4757..bfb69362fac6e 100644 --- a/ee/packages/omnichannel-services/src/QueueWorker.ts +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -35,7 +35,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { async started(): Promise { try { - this.shouldWork = await License.hasLicense('scalability'); + this.shouldWork = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index bf50d15ae71b2..1da194044df77 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,87 @@ # @rocket.chat/pdf-worker +## 0.0.15 + +### Patch Changes + +- 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.0.15-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + +## 0.0.14-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 + +## 0.0.14-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 + +## 0.0.14-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 + +## 0.0.14-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 + +## 0.0.14-rc.0 + +### Patch Changes + +- 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.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 + +- @rocket.chat/core-typings@6.3.5 + +## 0.0.10 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.4 + ## 0.0.9 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 5e8324613ad1f..9081c64fba34d 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.0.9", + "version": "0.0.15", "private": true, "devDependencies": { "@storybook/addon-essentials": "~6.5.16", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 1ed57117b68e4..4ee4d1a7df521 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,116 @@ # @rocket.chat/presence +## 0.0.15 + +### Patch Changes + +- d9a150000d: Fixed presence broadcast being disabled on server restart +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/core-services@0.2.0 + - @rocket.chat/models@0.0.15 + +## 0.0.15-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/core-services@0.2.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.0.14-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/core-services@0.2.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.0.14-rc.3 + +### Patch Changes + +- d9a150000d: Fixed presence broadcast being disabled on server restart + - @rocket.chat/core-typings@6.4.0-rc.3 + - @rocket.chat/core-services@0.2.0-rc.3 + - @rocket.chat/models@0.0.14-rc.3 + +## 0.0.14-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/core-services@0.2.0-rc.2 +- @rocket.chat/models@0.0.14-rc.2 + +## 0.0.14-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/core-services@0.2.0-rc.1 +- @rocket.chat/models@0.0.11-rc.1 + +## 0.0.14-rc.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [982ef6f459] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/core-services@0.2.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.0.13 + +### Patch Changes + +- c655be17ca: Fixed presence broadcast being disabled on server restart + - @rocket.chat/core-typings@6.3.7 + - @rocket.chat/core-services@0.1.7 + - @rocket.chat/models@0.0.13 + +## 0.0.12 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/core-services@0.1.6 +- @rocket.chat/models@0.0.12 + +## 0.0.11 + +### Patch Changes + +- @rocket.chat/models@0.0.11 +- @rocket.chat/core-services@0.1.5 +- @rocket.chat/core-typings@6.3.5 + +## 0.0.10 + +### Patch Changes + +- @rocket.chat/models@0.0.10 +- @rocket.chat/core-services@0.1.4 +- @rocket.chat/core-typings@6.3.4 + ## 0.0.9 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 5f96bf0fde989..da77719abc88c 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.0.9", + "version": "0.0.15", "private": true, "devDependencies": { "@babel/core": "~7.22.9", @@ -28,10 +28,13 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../../package.json" + }, "dependencies": { "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/models": "workspace:^", - "mongodb": "^4.12.1" + "mongodb": "^4.17.1" } } diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 2f30a2ea8912d..fb656fc3e1580 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -39,9 +39,14 @@ export class Presence extends ServiceClass implements IPresence { } }); - this.onEvent('license.module', ({ module, valid }) => { + this.onEvent('license.module', async ({ module, valid }) => { if (module === 'scalability') { this.hasLicense = valid; + + // broadcast should always be enabled if license is active (unless the troubleshoot setting is on) + if (!this.broadcastEnabled && valid) { + await this.toggleBroadcast(true); + } } }); } @@ -58,9 +63,9 @@ export class Presence extends ServiceClass implements IPresence { }, 10000); try { - this.hasLicense = await License.hasLicense('scalability'); - await Settings.updateValueById('Presence_broadcast_disabled', false); + + this.hasLicense = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/ee/packages/ui-theming/CHANGELOG.md b/ee/packages/ui-theming/CHANGELOG.md new file mode 100644 index 0000000000000..134f170ce82a7 --- /dev/null +++ b/ee/packages/ui-theming/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/ui-theming + +## 0.1.0 + +### Minor Changes + +- 357a3a50fa: feat: high-contrast theme + +## 0.1.0-rc.0 + +### Minor Changes + +- 357a3a50fa: feat: high-contrast theme diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index b390d1563a535..11aa5fd57ff86 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -1,12 +1,12 @@ { "name": "@rocket.chat/ui-theming", - "version": "0.0.1", + "version": "0.1.0", "private": true, "devDependencies": { "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage-hooks": "^0.32.1", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/ui-contexts": "workspace:~", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/ee/packages/ui-theming/src/PaletteStyleTag.tsx b/ee/packages/ui-theming/src/PaletteStyleTag.tsx index 52d9123889872..ce911ce4a122e 100644 --- a/ee/packages/ui-theming/src/PaletteStyleTag.tsx +++ b/ee/packages/ui-theming/src/PaletteStyleTag.tsx @@ -3,7 +3,6 @@ import { createPortal } from 'react-dom'; import { codeBlock } from './codeBlockStyles'; import { convertToCss } from './helpers/convertToCss'; -import { filterOnlyChangedColors } from './helpers/filterOnlyChangedColors'; import { useCreateStyleContainer } from './hooks/useCreateStyleContainer'; import { useThemeMode } from './hooks/useThemeMode'; import { defaultPalette } from './palette'; @@ -20,9 +19,9 @@ export const PaletteStyleTag = memo(function PaletteStyleTag() { if (theme === 'high-contrast') { return paletteHighContrast; } - return {}; + return defaultPalette; }; - const palette = convertToCss(filterOnlyChangedColors(defaultPalette, getPalette()), '.rcx-content--main'); + const palette = convertToCss(getPalette(), '.rcx-content--main, .rcx-tile'); return createPortal(theme === 'dark' ? palette + codeBlock : palette, useCreateStyleContainer('main-palette')); }); diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 4825beec0cff6..5d207d63bc21d 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -44,10 +44,10 @@ export const palette = [ description: 'These should be applied according to surfaces', list: [ { name: 'font-white', token: 'white', color: '#FFFFFF' }, - { name: 'font-disabled', token: 'N100', color: '#F7F8FA' }, + { name: 'font-disabled', token: 'N500', color: '#CBCED1' }, { name: 'font-annotation', token: 'N600', color: '#9EA2A8' }, - { name: 'font-hint', token: 'N700', color: '#6C727A' }, - { name: 'font-secondary-info', token: 'N700', color: '#6C727A' }, + { name: 'font-hint', token: 'N700', color: '#6C737A' }, + { name: 'font-secondary-info', token: 'N700', color: '#6C737A' }, { name: 'font-default', token: 'N800', color: '#2F343D' }, { name: 'font-titles-labels', token: 'N900', color: '#1F2329' }, { name: 'font-info', token: 'P600', color: '#095AD2' }, @@ -76,7 +76,7 @@ export const palette = [ { name: 'status-font-on-info', token: 'P600', color: '#095AD2' }, { name: 'status-font-on-success', token: 'S800', color: '#148660' }, { name: 'status-font-on-danger', token: 'D800', color: '#9B1325' }, - { name: 'status-font-on-warning', token: 'W900', color: '#B88D00' }, + { name: 'status-font-on-warning', token: 'W900', color: '#8E6300' }, { name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' }, { name: 'status-font-on-service-1', token: 'S1-800', color: '#974809' }, { name: 'status-font-on-service-2 ', token: 'S2-600', color: '#7F1B9F' }, @@ -88,22 +88,22 @@ export const palette = [ description: 'Badge Background', list: [ { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, - { name: 'badge-background-level-1', token: 'N700', color: '#6C727A' }, - { name: 'badge-background-level-2', token: '', color: '#1D74F5' }, + { name: 'badge-background-level-1', token: 'N700', color: '#6C737A' }, + { name: 'badge-background-level-2', token: '', color: '#156FF5' }, { name: 'badge-background-level-3', token: '', color: '#F38C39' }, - { name: 'badge-background-level-4', token: '', color: '#F5455C' }, + { name: 'badge-background-level-4', token: '', color: '#EC0D2A' }, ], }, { category: 'Status Bullet', description: 'Used to show user status', list: [ - { name: 'status-bullet-online', token: '', color: '#158D65' }, + { name: 'status-bullet-online', token: '', color: '#148660' }, { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#DA1F37' }, + { name: 'status-bullet-busy', token: '', color: '#D40C26' }, { name: 'status-bullet-disabled', token: '', color: '#F38C39' }, - { name: 'status-bullet-offline', token: '', color: '#AC892F' }, - { name: 'status-bullet-loading', token: '', color: '#9ea2a8' }, + { name: 'status-bullet-offline', token: '', color: '#6C737A' }, + { name: 'status-bullet-loading', token: '', color: '#6C737A' }, ], }, { @@ -122,7 +122,7 @@ export const palette = [ list: [ { name: 'button-background-primary-default', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-hover', token: 'P600', color: '#095AD2' }, - { name: 'button-background-primary-press', token: 'P700', color: '#095AD2' }, + { name: 'button-background-primary-press', token: 'P700', color: '#10529E' }, { name: 'button-background-primary-focus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-keyfocus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-disabled', token: 'P200', color: '#D1EBFE' }, @@ -133,7 +133,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' }, @@ -144,7 +144,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-danger-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-danger-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-danger-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-disabled', token: 'N300', color: '#EEEFF1' }, @@ -164,11 +164,11 @@ export const palette = [ { description: 'Success Background', list: [ - { name: 'button-background-success-default', token: '', color: '#158D65' }, + { name: 'button-background-success-default', token: '', color: '#148660' }, { name: 'button-background-success-hover', token: 'S900', color: '#106D4F' }, { name: 'button-background-success-press', token: 'S1000', color: '#0D5940' }, - { name: 'button-background-success-focus', token: '', color: '#158D65' }, - { name: 'button-background-success-keyfocus', token: '', color: '#158D65' }, + { name: 'button-background-success-focus', token: '', color: '#148660' }, + { name: 'button-background-success-keyfocus', token: '', color: '#148660' }, { name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' }, ], }, @@ -179,7 +179,7 @@ export const palette = [ { name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-secondary', token: 'N900', color: '#1F2329' }, { name: 'button-font-on-secondary-disabled', token: 'N600', color: '#CBCED1' }, - { name: 'button-font-on-secondary-danger', token: 'D900', color: '#BB0B21' }, + { name: 'button-font-on-secondary-danger', token: '', color: '#BB0B21' }, { name: 'button-font-on-secondary-danger-disabled', token: 'D300', @@ -187,7 +187,7 @@ export const palette = [ }, { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-danger-disabled', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-success', token: '', color: '#EBECEF' }, + { name: 'button-font-on-success', token: '', color: '#FFFFFF' }, { name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' }, ], }, diff --git a/ee/packages/ui-theming/src/paletteDark.ts b/ee/packages/ui-theming/src/paletteDark.ts index 545fc9f098b80..89ac7817be42a 100644 --- a/ee/packages/ui-theming/src/paletteDark.ts +++ b/ee/packages/ui-theming/src/paletteDark.ts @@ -9,7 +9,7 @@ export const palette = [ { name: 'stroke-dark', token: 'N600', color: '#9EA2A8' }, { name: 'stroke-extra-dark', token: 'N400', color: '#CBCED1' }, { name: 'stroke-extra-light-highlight', token: '', color: '#87CBFC' }, - { name: 'stroke-highlight', token: '', color: '#3976D1' }, + { name: 'stroke-highlight', token: '', color: '#6292DA' }, { name: 'stroke-extra-light-error', token: '', color: '#F49AA6' }, { name: 'stroke-error', token: '', color: '#BB3E4E' }, ], @@ -51,7 +51,7 @@ export const palette = [ { name: 'font-default', token: 'N400', color: '#E4E7EA' }, { name: 'font-titles-labels', token: '', color: '#F2F3F5' }, { name: 'font-info', token: '', color: '#739EDE' }, - { name: 'font-danger', token: '', color: '#CF6E7A' }, + { name: 'font-danger', token: '', color: '#D88892' }, { name: 'font-pure-black', token: '', color: '#2F343D' }, { name: 'font-pure-white', token: '', color: '#FFFFFF' }, ], @@ -120,12 +120,12 @@ export const palette = [ category: 'Button', description: 'Primary Background', list: [ - { name: 'button-background-primary-default', token: '', color: '#3976D1' }, - { name: 'button-background-primary-hover', token: '', color: '#295EAE' }, - { name: 'button-background-primary-press', token: '', color: '#245399' }, - { name: 'button-background-primary-focus', token: '', color: '#3976D1' }, - { name: 'button-background-primary-keyfocus', token: '', color: '#3976D1' }, - { name: 'button-background-primary-disabled', token: '', color: '#1D3963' }, + { name: 'button-background-primary-default', token: '', color: '#095AD2' }, + { name: 'button-background-primary-hover', token: '', color: '#10529E' }, + { name: 'button-background-primary-press', token: '', color: '#01336B' }, + { name: 'button-background-primary-focus', token: '', color: '#095AD2' }, + { name: 'button-background-primary-keyfocus', token: '', color: '#095AD2' }, + { name: 'button-background-primary-disabled', token: '', color: '#012247' }, ], }, { @@ -177,7 +177,7 @@ export const palette = [ list: [ { name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-secondary', token: 'N400', color: '#E4E7EA' }, - { name: 'button-font-on-secondary-danger', token: '', color: '#C14454' }, + { name: 'button-font-on-secondary-danger', token: '', color: '#FFC1C9' }, { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-success', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-primary-disabled', token: 'N700', color: '#6C727A' }, @@ -185,7 +185,7 @@ export const palette = [ { name: 'button-font-on-secondary-danger-disabled', token: '', - color: '#613339', + color: '#6B0513', }, { name: 'button-font-on-danger-disabled', token: '', color: '#757575' }, { name: 'button-font-on-success-disabled', token: '', color: '#757575' }, diff --git a/package.json b/package.json index 47f10019e2ccd..962e42d48c6e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.4.0-develop", + "version": "6.5.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, @@ -21,7 +21,7 @@ "@types/chart.js": "^2.9.37", "@types/js-yaml": "^4.0.5", "husky": "^7.0.4", - "turbo": "^1.10.13" + "turbo": "~1.10.14" }, "workspaces": [ "apps/*", @@ -63,6 +63,7 @@ "minimist": "1.2.6", "adm-zip": "0.5.9", "preact@10.15.1": "patch:preact@npm:10.15.1#.yarn/patches/preact-npm-10.15.1-bd458de913.patch", - "@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": "patch:@storybook/react-docgen-typescript-plugin@npm%3A1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0#./.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch" + "@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": "patch:@storybook/react-docgen-typescript-plugin@npm%3A1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0#./.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch", + "mongodb@^4.17.1": "patch:mongodb@npm:4.17.1#.yarn/patches/mongodb-npm-4.17.1-a2fe811ff1.patch" } } diff --git a/packages/agenda/package.json b/packages/agenda/package.json index dfdeb009c0905..f533e05dba28a 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -9,7 +9,7 @@ "debug": "~4.1.1", "human-interval": "^2.0.1", "moment-timezone": "~0.5.43", - "mongodb": "^4.12.1" + "mongodb": "^4.17.1" }, "devDependencies": { "@types/debug": "^4.1.8", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 07a137608d92f..940c05513ec15 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,137 @@ # @rocket.chat/core-services +## 0.2.0 + +### Minor Changes + +- 982ef6f459: Add new event to notify users directly about new banners +- 19aec23cda: New AddUser workflow for Federated Rooms + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/rest-typings@6.4.0 + - @rocket.chat/models@0.0.15 + +## 0.2.0-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/rest-typings@6.4.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.2.0-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/rest-typings@6.4.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.2.0-rc.3 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.3 +- @rocket.chat/rest-typings@6.4.0-rc.3 +- @rocket.chat/models@0.0.14-rc.3 + +## 0.2.0-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/rest-typings@6.4.0-rc.2 +- @rocket.chat/models@0.0.14-rc.2 + +## 0.2.0-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/rest-typings@6.4.0-rc.1 +- @rocket.chat/models@0.0.14-rc.1 + +## 0.2.0-rc.0 + +### Minor Changes + +- 982ef6f459: Add new event to notify users directly about new banners +- 19aec23cda: New AddUser workflow for Federated Rooms + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [2db32f0d4a] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [19aec23cda] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [357a3a50fa] +- Updated dependencies [61128364d6] +- Updated dependencies [9496f1eb97] +- Updated dependencies [d45365436e] +- Updated dependencies [93d4912e17] + - @rocket.chat/core-typings@6.4.0-rc.0 + - @rocket.chat/rest-typings@6.4.0-rc.0 + - @rocket.chat/models@0.0.11-rc.0 + +## 0.1.8 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.8 +- @rocket.chat/rest-typings@6.3.8 +- @rocket.chat/models@0.0.14 + +## 0.1.7 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.7 +- @rocket.chat/rest-typings@6.3.7 +- @rocket.chat/models@0.0.13 + +## 0.1.6 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/rest-typings@6.3.6 +- @rocket.chat/models@0.0.12 + +## 0.1.5 + +### Patch Changes + +- @rocket.chat/models@0.0.11 +- @rocket.chat/core-typings@6.3.5 +- @rocket.chat/rest-typings@6.3.5 + +## 0.1.4 + +### Patch Changes + +- @rocket.chat/models@0.0.10 +- @rocket.chat/core-typings@6.3.4 +- @rocket.chat/rest-typings@6.3.4 + ## 0.1.3 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index d63c525a2782c..7f6a82642d47d 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.1.3", + "version": "0.2.0", "private": true, "devDependencies": { "@babel/core": "~7.22.9", @@ -13,7 +13,7 @@ "babel-jest": "^29.5.0", "eslint": "~8.45.0", "jest": "~29.6.1", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "prettier": "~2.8.8", "typescript": "~5.2.2" }, @@ -30,14 +30,17 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../package.json" + }, "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/message-parser": "next", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "@types/fibers": "^3.1.1", "fibers": "^5.0.3" } diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index 88ca1034b9c98..e2a7f624d8df3 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -32,6 +32,7 @@ import type { ILivechatInquiryRecord, ILivechatAgent, IBanner, + ILivechatVisitor, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -242,7 +243,8 @@ export type EventSignatures = { data: | { type: 'agentStatus'; status: string } | { type: 'queueData'; data: { [k: string]: unknown } | undefined } - | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } }, + | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } } + | { type: 'visitorData'; visitor: ILivechatVisitor }, ): void; // Send all events from here diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index 66806dc54fdea..f0b5e67594c2c 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -46,7 +46,11 @@ export class Api implements IApiService { } async broadcast(event: T, ...args: Parameters): Promise { - return this.broker?.broadcast(event, ...args); + if (!this.broker) { + throw new Error(`No broker set to broadcast: ${event}`); + } + + return this.broker.broadcast(event, ...args); } async broadcastToServices( diff --git a/packages/core-services/src/types/IBannerService.ts b/packages/core-services/src/types/IBannerService.ts index 1035bdd59510e..50b8ab08275cd 100644 --- a/packages/core-services/src/types/IBannerService.ts +++ b/packages/core-services/src/types/IBannerService.ts @@ -2,7 +2,7 @@ import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typing export interface IBannerService { getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise; - create(banner: Optional): Promise; + create(banner: Optional): Promise; dismiss(userId: string, bannerId: string): Promise; discardDismissal(bannerId: string): Promise; getById(bannerId: string): Promise; diff --git a/packages/core-services/src/types/IBroker.ts b/packages/core-services/src/types/IBroker.ts index 8647d04a56dc5..4bd48afef0ff0 100644 --- a/packages/core-services/src/types/IBroker.ts +++ b/packages/core-services/src/types/IBroker.ts @@ -27,6 +27,7 @@ export type BaseMetricOptions = { labelNames?: Array; unit?: string; aggregator?: string; + [key: string]: unknown; }; export interface IServiceMetrics { diff --git a/packages/core-services/src/types/ILicense.ts b/packages/core-services/src/types/ILicense.ts index 7b89a006bfc0b..c9247f8887ce9 100644 --- a/packages/core-services/src/types/ILicense.ts +++ b/packages/core-services/src/types/ILicense.ts @@ -1,9 +1,9 @@ import type { IServiceClass } from './ServiceClass'; export interface ILicense extends IServiceClass { - hasLicense(feature: string): boolean; + hasModule(feature: string): boolean; - isEnterprise(): boolean; + hasValidLicense(): boolean; getModules(): string[]; diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index ea8f207df67d5..b38d6a9559d66 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -9,6 +9,7 @@ export interface IMessageService { user: Pick, extraData?: Partial, ): Promise; + beforeSave(param: { message: IMessage; room: IRoom; user: IUser }): Promise; sendMessageWithValidation(user: IUser, message: Partial, room: Partial, upsert?: boolean): Promise; deleteMessage(user: IUser, message: IMessage): Promise; updateMessage(message: IMessage, user: IUser, originalMsg?: IMessage): Promise; diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index e69707e18a365..f7be69ce2a7c7 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -4,6 +4,7 @@ export interface ISubscriptionExtraData { open: boolean; ls?: Date; prid?: string; + roles?: string[]; } interface ICreateRoomOptions extends Partial> { @@ -52,4 +53,5 @@ export interface IRoomService { sendMessage?: boolean, ): Promise; getRouteLink(room: AtLeast): Promise; + join(param: { room: IRoom; user: Pick; joinCode?: string }): Promise; } diff --git a/packages/core-typings/.eslintrc.json b/packages/core-typings/.eslintrc.json index 56a6f6602e335..44d74e043bc46 100644 --- a/packages/core-typings/.eslintrc.json +++ b/packages/core-typings/.eslintrc.json @@ -8,5 +8,8 @@ } } ], - "ignorePatterns": ["**/dist"] + "ignorePatterns": ["**/dist"], + "rules": { + "@typescript-eslint/no-empty-interface": "off" + } } diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 5e6880b649a4c..f6c174da450f4 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,60 @@ # @rocket.chat/core-typings +## 6.4.0 + +### Minor Changes + +- 239a34e877: new: ring mobile users on direct conference calls +- 4186eecf05: Introduce the ability to report an user +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel +- 1041d4d361: Added option to select between two script engine options for the integrations + +### Patch Changes + +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- ba24f3c21f: Fixed `default` field not being returned from the `setDefault` endpoints when setting to false +- 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. +- d45365436e: Use group filter when set to LDAP sync process + +## 6.4.0-rc.5 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations + +## 6.4.0-rc.4 + +## 6.4.0-rc.3 + +## 6.4.0-rc.2 + +## 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 +- ebab8c4dd8: Added Reports Metrics Dashboard to Omnichannel + +### Patch Changes + +- 203304782f: Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints +- ba24f3c21f: Fixed `default` field not being returned from the `setDefault` endpoints when setting to false +- 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. +- d45365436e: Use group filter when set to LDAP sync process + +## 6.3.8 + +## 6.3.7 + +## 6.3.6 + +## 6.3.5 + +## 6.3.4 + ## 6.3.3 ## 6.3.2 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 33c93c28d8a88..60874e11c810e 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,10 +1,11 @@ { + "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.3.3", + "version": "6.4.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", - "mongodb": "^4.12.1", + "mongodb": "^4.17.1", "prettier": "~2.8.8", "typescript": "~5.2.2" }, @@ -22,9 +23,9 @@ ], "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/message-parser": "next", - "@rocket.chat/ui-kit": "next" + "@rocket.chat/ui-kit": "^0.32.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/core-typings/src/ICloud.ts b/packages/core-typings/src/ICloud.ts index ce6b97ce7d773..6710e7ced9d29 100644 --- a/packages/core-typings/src/ICloud.ts +++ b/packages/core-typings/src/ICloud.ts @@ -1,5 +1,4 @@ export type CloudRegistrationStatus = { - connectToCloud: boolean; email: string; token: string; uniqueId: string; diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index 6b99424264b27..cffff75767f4d 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -1,6 +1,8 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IUser } from './IUser'; +export type IntegrationScriptEngine = 'vm2' | 'isolated-vm'; + export interface IIncomingIntegration extends IRocketChatRecord { type: 'webhook-incoming'; _createdBy: Pick | null; @@ -22,6 +24,8 @@ export interface IIncomingIntegration extends IRocketChatRecord { alias?: string; avatar?: string; emoji?: string; + + scriptEngine?: IntegrationScriptEngine; } export type OutgoingIntegrationEvent = @@ -65,6 +69,8 @@ export interface IOutgoingIntegration extends IRocketChatRecord { alias?: string; avatar?: string; emoji?: string; + + scriptEngine?: IntegrationScriptEngine; } export type IIntegration = IIncomingIntegration | IOutgoingIntegration; diff --git a/packages/core-typings/src/IIntegrationHistory.ts b/packages/core-typings/src/IIntegrationHistory.ts index 6297cd7d74a0f..6594d611fb496 100644 --- a/packages/core-typings/src/IIntegrationHistory.ts +++ b/packages/core-typings/src/IIntegrationHistory.ts @@ -1,3 +1,4 @@ +import type { IMessage } from './IMessage'; import type { IRocketChatRecord } from './IRocketChatRecord'; export interface IIntegrationHistory extends IRocketChatRecord { @@ -17,10 +18,10 @@ export interface IIntegrationHistory extends IRocketChatRecord { finished: boolean; triggerWord?: string; - prepareSentMessage?: string; - processSentMessage?: string; + prepareSentMessage?: { channel: string; message: Partial }[]; + processSentMessage?: { channel: string; message: Partial }[]; url?: string; - httpCallData?: string; + httpCallData?: Record; httpError?: any; httpResult?: string; error?: any; diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts index d22ea36aa7c62..21819cc23f246 100644 --- a/packages/core-typings/src/ILivechatVisitor.ts +++ b/packages/core-typings/src/ILivechatVisitor.ts @@ -7,7 +7,7 @@ export interface IVisitorPhone { export interface IVisitorLastChat { _id: string; - ts: string; + ts: Date; } export interface ILivechatVisitorConnectionData { @@ -47,6 +47,8 @@ export interface ILivechatVisitor extends IRocketChatRecord { contactManager?: { username: string; }; + activity?: string[]; + disabled?: boolean; } export interface ILivechatVisitorDTO { diff --git a/packages/core-typings/src/IOembed.ts b/packages/core-typings/src/IOembed.ts index c540cb8938171..0b781aa07fc85 100644 --- a/packages/core-typings/src/IOembed.ts +++ b/packages/core-typings/src/IOembed.ts @@ -1,9 +1,5 @@ -import type Url from 'url'; - import type { MessageAttachment } from './IMessage'; -export type ParsedUrl = Pick; - export type OEmbedMeta = { [key: string]: string; } & { @@ -12,8 +8,7 @@ export type OEmbedMeta = { }; export type OEmbedUrlContent = { - urlObj: Url.UrlWithParsedQuery; - parsedUrl: ParsedUrl; + urlObj: URL; headers: { [k: string]: string }; body: string; statusCode: number; @@ -27,7 +22,6 @@ export type OEmbedProvider = { export type OEmbedUrlContentResult = { headers: { [key: string]: string }; body: string; - parsedUrl: Pick; statusCode: number; attachments?: MessageAttachment[]; }; @@ -38,7 +32,6 @@ export type OEmbedUrlWithMetadata = { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: Pick; content: OEmbedUrlContent; }; diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 4ce8b84abb0e0..523450e9594da 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -151,7 +151,11 @@ export enum OmnichannelSourceType { export interface IOmnichannelGenericRoom extends Omit { t: 'l' | 'v'; - v: Pick & { lastMessageTs?: Date; phone?: string }; + v: Pick & { + lastMessageTs?: Date; + phone?: string; + activity?: string[]; + }; email?: { // Data used when the room is created from an email, via email Integration. inbox: string; @@ -193,8 +197,24 @@ export interface IOmnichannelGenericRoom extends Omit; invalidValue?: SettingValue; - valueSource?: string; + valueSource?: 'packageValue' | 'processEnvValue'; secret?: boolean; i18nDescription?: string; autocomplete?: boolean; diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 2ea8115a727c9..443cbfb239578 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -3,11 +3,22 @@ import type { CpuInfo } from 'os'; import type { DeviceSessionAggregationResult, OSSessionAggregationResult, UserSessionAggregationResult } from './ISession'; import type { ISettingStatisticsObject } from './ISetting'; import type { ITeamStats } from './ITeam'; +import type { MACStats } from './omnichannel'; export interface IStats { _id: string; - wizard: Record; + wizard: { + organizationType?: string; + industry?: string; + size?: string; + country?: string; + language?: string; + serverType?: string; + registerServer?: boolean; + }; uniqueId: string; + deploymentFingerprintHash: string; + deploymentFingerprintVerified: boolean; installedAt?: string; version?: string; tag?: string; @@ -85,6 +96,10 @@ export interface IStats { mongoStorageEngine: string; pushQueue: number; omnichannelSources: { [key: string]: number | string }[]; + omnichannelContactsBySource: MACStats; + uniqueContactsOfLastMonth: MACStats; + uniqueContactsOfLastWeek: MACStats; + uniqueContactsOfYesterday: MACStats; departments: number; archivedDepartments: number; routingAlgorithm: string; @@ -203,6 +218,7 @@ export interface IStats { totalCustomRoles: number; totalWebRTCCalls: number; uncaughtExceptionsCount: number; + push: number; matrixFederation: { enabled: boolean; }; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts new file mode 100644 index 0000000000000..3d891daf132ff --- /dev/null +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import { type UiKitPayload } from '../UIKit'; + +type TargetPlatform = 'web' | 'mobile'; + +type Dictionary = { + [lng: string]: { + [key: string]: string; + }; +}; + +type Creator = 'cloud' | 'system'; + +export interface Announcement extends IRocketChatRecord { + selector?: { + roles?: string[]; + }; + platform: TargetPlatform[]; + expireAt: Date; + startAt: Date; + createdBy: Creator; + createdAt: Date; + dictionary?: Dictionary; + view: UiKitPayload; + surface: 'banner' | 'modal'; +} diff --git a/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts new file mode 100644 index 0000000000000..fff1db8f1b998 --- /dev/null +++ b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface NpsSurveyAnnouncement { + id: string; + startAt: Date; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts new file mode 100644 index 0000000000000..7e81e1b475996 --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface WorkspaceLicensePayload { + version: number; + address: string; + license: string; + updatedAt: Date; + modules: string; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts new file mode 100644 index 0000000000000..fb95cfa4553c0 --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IBanner } from '../IBanner'; +import type { Announcement } from './Announcement'; +import type { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; + +export interface WorkspaceSyncPayload { + workspaceId: string; + publicKey?: string; + trial?: { + trialing: boolean; + trialID: string; + endDate: Date; + marketing: { + utmContent: string; + utmMedium: string; + utmSource: string; + utmCampaign: string; + }; + DowngradesToPlan: { + id: string; + }; + trialRequested: boolean; + }; + /** @deprecated */ + nps?: NpsSurveyAnnouncement; + /** @deprecated */ + banners?: IBanner[]; +} + +export interface WorkspaceSyncRequestPayload { + uniqueId: string; + workspaceId: string; + seats: number; + MAC: number; // Need to align on the property + address: string; + siteName: string; + deploymentMethod: string; + deploymentPlatform: string; + version: string; + licenseVersion: number; + connectionDisable: boolean; +} + +export interface WorkspaceSyncResponse { + workspaceId: string; + publicKey: string; + license: unknown; +} + +export interface WorkspaceCommsRequestPayload { + npsEnabled: boolean; + deploymentMethod: string; + deploymentPlatform: string; + version: string; +} +export interface WorkspaceCommsResponsePayload { + nps?: NpsSurveyAnnouncement | null; // Potentially consolidate into announcements + announcements?: { + create: Announcement[]; + delete: Announcement['_id'][]; + }; +} diff --git a/packages/core-typings/src/cloud/index.ts b/packages/core-typings/src/cloud/index.ts new file mode 100644 index 0000000000000..da0565a215ed5 --- /dev/null +++ b/packages/core-typings/src/cloud/index.ts @@ -0,0 +1,10 @@ +export { Announcement } from './Announcement'; +export { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; +export { WorkspaceLicensePayload } from './WorkspaceLicensePayload'; +export { + WorkspaceSyncPayload, + WorkspaceSyncRequestPayload, + WorkspaceSyncResponse, + WorkspaceCommsRequestPayload, + WorkspaceCommsResponsePayload, +} from './WorkspaceSyncPayload'; diff --git a/packages/core-typings/src/ee/ILicense/ILicense.ts b/packages/core-typings/src/ee/ILicense/ILicense.ts deleted file mode 100644 index 8490ab1d7cbe6..0000000000000 --- a/packages/core-typings/src/ee/ILicense/ILicense.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ILicenseTag } from './ILicenseTag'; - -export interface ILicense { - url: string; - expiry: string; - maxActiveUsers: number; - modules: string[]; - maxGuestUsers: number; - maxRoomsPerGuest: number; - tag?: ILicenseTag; - meta?: { - trial: boolean; - trialEnd: string; - workspaceId: string; - }; - apps?: { - maxPrivateApps: number; - maxMarketplaceApps: number; - }; -} diff --git a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts b/packages/core-typings/src/ee/ILicense/ILicenseTag.ts deleted file mode 100644 index 2f11fdebd5db5..0000000000000 --- a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ILicenseTag { - name: string; - color: string; -} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 8cd004dd09f14..de36606e7f90d 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -39,7 +39,6 @@ export * from './IUserSession'; export * from './IUserStatus'; export * from './IUser'; -export * from './ee/ILicense/ILicense'; export * from './ee/IAuditLog'; export * from './import'; @@ -135,3 +134,5 @@ export * from './ICustomOAuthConfig'; export * from './IModerationReport'; export * from './CustomFieldMetadata'; + +export * as Cloud from './cloud'; diff --git a/packages/core-typings/src/migrations/IControl.ts b/packages/core-typings/src/migrations/IControl.ts index 9ff9937035505..3f89ce730f1a7 100644 --- a/packages/core-typings/src/migrations/IControl.ts +++ b/packages/core-typings/src/migrations/IControl.ts @@ -2,6 +2,7 @@ export type IControl = { _id: string; version: number; locked: boolean; + hash?: string; buildAt?: string | Date; lockedAt?: string | Date; }; diff --git a/packages/core-typings/src/omnichannel/index.ts b/packages/core-typings/src/omnichannel/index.ts index 703cf3b4ca77a..c6235175dafc6 100644 --- a/packages/core-typings/src/omnichannel/index.ts +++ b/packages/core-typings/src/omnichannel/index.ts @@ -2,3 +2,4 @@ export * from './sms'; export * from './routing'; export * from './queue'; export * from './reports'; +export * from './mac'; diff --git a/packages/core-typings/src/omnichannel/mac.ts b/packages/core-typings/src/omnichannel/mac.ts new file mode 100644 index 0000000000000..8591edbb02878 --- /dev/null +++ b/packages/core-typings/src/omnichannel/mac.ts @@ -0,0 +1,5 @@ +export type MACStats = { + contactsCount: number; + conversationsCount: number; + sources: { source: string; contactsCount: number; conversationsCount: number }[]; +}; diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 44b205dda1e79..b0cf975ee4d8d 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,11 +1,125 @@ # @rocket.chat/cron +## 0.0.11 + +### Patch Changes + +- 61a106fbf2: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + +- 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 + - @rocket.chat/models@0.0.15 + - @rocket.chat/random@1.2.1 + +## 0.0.11-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/models@0.0.15-rc.5 + +## 0.0.10-rc.4 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.4 +- @rocket.chat/models@0.0.14-rc.4 + +## 0.0.11-rc.3 + +### Patch Changes + +- 61a106fbf2: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + + - @rocket.chat/core-typings@6.4.0-rc.3 + - @rocket.chat/models@0.0.14-rc.3 + +## 0.0.11-rc.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.2 +- @rocket.chat/models@0.0.14-rc.2 + +## 0.0.11-rc.1 + +### Patch Changes + +- @rocket.chat/core-typings@6.4.0-rc.1 +- @rocket.chat/models@0.0.14-rc.1 + +## 0.0.11-rc.0 + +### Patch Changes + +- 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 + - @rocket.chat/models@0.0.11-rc.0 + - @rocket.chat/random@1.2.1 + +## 0.0.10 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.8 +- @rocket.chat/models@0.0.14 + +## 0.0.9 + +### Patch Changes + +- deffcb187c: Increase cron job check delay to 1 min from 5s. + + This reduces MongoDB requests introduced on 6.3. + + - @rocket.chat/core-typings@6.3.7 + - @rocket.chat/models@0.0.13 + +## 0.0.8 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.6 +- @rocket.chat/models@0.0.12 + +## 0.0.7 + +### Patch Changes + +- @rocket.chat/models@0.0.11 +- @rocket.chat/core-typings@6.3.5 + +## 0.0.6 + +### Patch Changes + +- @rocket.chat/models@0.0.10 +- @rocket.chat/core-typings@6.3.4 + ## 0.0.5 ### Patch Changes - @rocket.chat/core-typings@6.3.3 -- @rocket.chat/models@0.0.9 +- @rocket.chat/models@0.0.10 ## 0.0.4 diff --git a/packages/cron/package.json b/packages/cron/package.json index 396047b3147c2..37e751fe03d5a 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.0.5", + "version": "0.0.11", "private": true, "devDependencies": { "@types/jest": "~29.5.3", @@ -26,6 +26,6 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/random": "workspace:^", - "mongodb": "^4.12.1" + "mongodb": "^4.17.1" } } diff --git a/packages/cron/src/index.ts b/packages/cron/src/index.ts index 62bc0512bac65..dcd4dc5745d80 100644 --- a/packages/cron/src/index.ts +++ b/packages/cron/src/index.ts @@ -64,6 +64,7 @@ export class AgendaCronJobs { mongo, db: { collection: 'rocketchat_cron' }, defaultConcurrency: 1, + processEvery: '1 minute', }); await this.scheduler.start(); diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md new file mode 100644 index 0000000000000..62704f6c7714f --- /dev/null +++ b/packages/eslint-config/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/eslint-config + +## 0.6.0 + +### Minor Changes + +- 0f56aacc4d: Unpublished changes in ESLint config + +## 0.6.0-rc.0 + +### Minor Changes + +- 0f56aacc4d: Unpublished changes in ESLint config diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 6c29a7d200a72..9b0adef2d80e8 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/eslint-config", - "version": "0.5.2", + "version": "0.6.0", "description": "Rocket.Chat's JS/TS ESLint config", "dependencies": { "@babel/core": "^7.20.7", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 6d52e9064b161..dd8a0fce97891 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,123 @@ # Change Log +## 2.0.0 + +### Minor Changes + +- 1246a21648: feat: Add missing variants to UIKit button +- f9a748526d: feat: Adding new UIKit components: Callout, Checkbox, Radio Button, Time Picker, Toast Bar, Toggle Switch, Tab Navigation + +### Patch Changes + +- dc1d8ce92e: feat(fuselage-ui-kit): Introduce `TabsNavigationBlock` +- dce4a829fa: Handle invalid context on `VideoConferenceBlock` component +- Updated dependencies [074db3b419] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [0f56aacc4d] + - @rocket.chat/ui-contexts@2.0.0 + - @rocket.chat/eslint-config@0.6.0 + - @rocket.chat/gazzodown@2.0.0 + - @rocket.chat/ui-video-conf@2.0.0 + +## 2.0.0-rc.5 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.5 +- @rocket.chat/ui-contexts@2.0.0-rc.5 +- @rocket.chat/ui-video-conf@2.0.0-rc.5 + +## 2.0.0-rc.4 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.4 +- @rocket.chat/ui-contexts@2.0.0-rc.4 +- @rocket.chat/ui-video-conf@2.0.0-rc.4 + +## 2.0.0-rc.3 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.3 +- @rocket.chat/ui-contexts@2.0.0-rc.3 +- @rocket.chat/ui-video-conf@2.0.0-rc.3 + +## 2.0.0-rc.2 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.2 +- @rocket.chat/ui-contexts@2.0.0-rc.2 +- @rocket.chat/ui-video-conf@2.0.0-rc.2 + +## 2.0.0-rc.1 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.1 +- @rocket.chat/ui-contexts@2.0.0-rc.1 +- @rocket.chat/ui-video-conf@2.0.0-rc.1 + +## 2.0.0-rc.0 + +### Minor Changes + +- 1246a21648: feat: Add missing variants to UIKit button +- f9a748526d: feat: Adding new UIKit components: Callout, Checkbox, Radio Button, Time Picker, Toast Bar, Toggle Switch, Tab Navigation + +### Patch Changes + +- dc1d8ce92e: feat(fuselage-ui-kit): Introduce `TabsNavigationBlock` +- dce4a829fa: Handle invalid context on `VideoConferenceBlock` component +- Updated dependencies [074db3b419] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [0f56aacc4d] + - @rocket.chat/ui-contexts@2.0.0-rc.0 + - @rocket.chat/eslint-config@0.6.0-rc.0 + - @rocket.chat/gazzodown@2.0.0-rc.0 + - @rocket.chat/ui-video-conf@2.0.0-rc.0 + +## 1.0.8 + +### Patch Changes + +- @rocket.chat/gazzodown@1.0.8 +- @rocket.chat/ui-contexts@1.0.8 +- @rocket.chat/ui-video-conf@1.0.8 + +## 1.0.7 + +### Patch Changes + +- @rocket.chat/gazzodown@1.0.7 +- @rocket.chat/ui-contexts@1.0.7 +- @rocket.chat/ui-video-conf@1.0.7 + +## 1.0.6 + +### Patch Changes + +- @rocket.chat/gazzodown@1.0.6 +- @rocket.chat/ui-contexts@1.0.6 +- @rocket.chat/ui-video-conf@1.0.6 + +## 1.0.5 + +### Patch Changes + +- @rocket.chat/gazzodown@1.0.5 +- @rocket.chat/ui-contexts@1.0.5 +- @rocket.chat/ui-video-conf@1.0.5 + +## 1.0.4 + +### Patch Changes + +- @rocket.chat/gazzodown@1.0.4 +- @rocket.chat/ui-contexts@1.0.4 +- @rocket.chat/ui-video-conf@1.0.4 + ## 1.0.3 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 0cc50b819e9e7..4555216c2d440 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "1.0.3", + "version": "2.0.0", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -39,16 +39,16 @@ }, "peerDependencies": { "@rocket.chat/apps-engine": "*", - "@rocket.chat/eslint-config": "*", + "@rocket.chat/eslint-config": "0.6.0", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/fuselage-polyfills": "*", "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-contexts": "1.0.3", + "@rocket.chat/ui-contexts": "2.0.0", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "1.0.3", + "@rocket.chat/ui-video-conf": "2.0.0", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" @@ -56,14 +56,14 @@ "devDependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "^0.32.0", "@rocket.chat/prettier-config": "next", "@rocket.chat/styled": "next", "@rocket.chat/ui-contexts": "workspace:^", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "@rocket.chat/ui-video-conf": "workspace:^", "@storybook/addon-essentials": "~6.5.16", "@storybook/addons": "~6.5.16", @@ -89,7 +89,7 @@ }, "dependencies": { "@rocket.chat/gazzodown": "workspace:^", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "^0.32.1", "tslib": "^2.5.3" }, "volta": { diff --git a/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx b/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx index bbeaf3c338c67..979e04e808c77 100644 --- a/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx +++ b/packages/fuselage-ui-kit/src/blocks/InputBlock.tsx @@ -1,4 +1,10 @@ -import { Field } from '@rocket.chat/fuselage'; +import { + Field, + FieldLabel, + FieldRow, + FieldError, + FieldHint, +} from '@rocket.chat/fuselage'; import * as UiKit from '@rocket.chat/ui-kit'; import type { ReactElement } from 'react'; import { memo, useMemo } from 'react'; @@ -28,19 +34,19 @@ const InputBlock = ({ return ( {block.label && ( - + {surfaceRenderer.renderTextObject( block.label, 0, UiKit.BlockContext.NONE )} - + )} - + {surfaceRenderer.renderInputBlockElement(inputElement, 0)} - - {error && {error}} - {block.hint && {block.hint}} + + {error && {error}} + {block.hint && {block.hint}} ); }; diff --git a/packages/fuselage-ui-kit/src/elements/ButtonElement.tsx b/packages/fuselage-ui-kit/src/elements/ButtonElement.tsx index c7fe05971e274..1f80358b3d933 100644 --- a/packages/fuselage-ui-kit/src/elements/ButtonElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/ButtonElement.tsx @@ -1,6 +1,6 @@ import { Button, Throbber } from '@rocket.chat/fuselage'; import * as UiKit from '@rocket.chat/ui-kit'; -import type { ReactElement } from 'react'; +import type { MouseEventHandler, ReactElement } from 'react'; import { useUiKitState } from '../hooks/useUiKitState'; import type { BlockProps } from '../utils/BlockProps'; @@ -15,6 +15,10 @@ const ButtonElement = ({ const [{ loading }, action] = useUiKitState(block, context); const { style, url, text, value, secondary } = block; + const handleClick: MouseEventHandler = (e) => { + action({ target: e.currentTarget }); + }; + if (url) { return ( + ), - 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 669b46571b0ef..9764c3c2ce372 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 6c3a7fd38957e..359a953bf1246 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 9f9e41810c7b6..671a5b343b216 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 2f5368d8729cb..3b2d36f3609a5 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 0000000000000..da1b812425742 --- /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 7f4c5ca340a46..ac7df77072b47 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 705fdcacc7dac..068c84c1c459c 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 98991ab72b53c..8bd9ac468c6e9 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 0000000000000..915bcabc5395c --- /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 d739e442879e5..c2aeb26653508 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 1d89b9145bc4d..8d03eaafa2081 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,124 @@ # @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 + +- 8a7d5d3898: fix: agent role being removed upon user deactivation + - @rocket.chat/core-typings@6.3.4 + ## 0.0.9 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 89ee934005cd7..5174bf22f97ae 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.9", + "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 23e77ff1de29c..a1874b1443479 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 62f33ef5d3b21..4fe496bb954c8 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 0000000000000..672ff8c316a0e --- /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/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index a074d5c311264..75fe0f54b2eb4 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -71,4 +71,6 @@ export interface ILivechatDepartmentModel extends IBaseModel; countArchived(): Promise; findEnabledInIds(departmentsIds: string[], options?: FindOptions): FindCursor; + archiveDepartment(_id: string): Promise; + unarchiveDepartment(_id: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index b2af0add69a78..20100cbb4f616 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 '..'; @@ -196,7 +203,7 @@ export interface ILivechatRoomsModel extends IBaseModel { options?: FindOptions, extraQuery?: Filter, ): FindCursor; - setResponseByRoomId(roomId: string, response: { user: { _id: string; username: string } }): Promise; + setResponseByRoomId(roomId: string, responseBy: IOmnichannelRoom['responseBy']): Promise; setNotResponseByRoomId(roomId: string): Promise; setAgentLastMessageTs(roomId: string): Promise; saveAnalyticsDataByRoomId( @@ -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 370db511dadfe..5c598c6a6a974 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/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index de0cf9d10f963..66ffe92327498 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -130,6 +130,8 @@ export interface IRoomsModel extends IBaseModel { findByE2E(options?: FindOptions): FindCursor; + findE2ERoomById(roomId: IRoom['_id'], options?: FindOptions): Promise; + findRoomsInsideTeams(autoJoin?: boolean): FindCursor; findOneDirectRoomContainingAllUserIDs(uid: IDirectMessageRoom['uids'], options?: FindOptions): Promise; diff --git a/packages/model-typings/src/models/ISessionsModel.ts b/packages/model-typings/src/models/ISessionsModel.ts index cebe0c861d3f9..1e6a36fd6f782 100644 --- a/packages/model-typings/src/models/ISessionsModel.ts +++ b/packages/model-typings/src/models/ISessionsModel.ts @@ -145,4 +145,8 @@ export interface ISessionsModel extends IBaseModel { }): Promise; createBatch(sessions: OptionalId[]): Promise; + + updateDailySessionById(_id: ISession['_id'], record: Partial): Promise; + + updateAllSessionsByDateToComputed({ start, end }: DestructuredRange): Promise; } diff --git a/packages/model-typings/src/models/ISettingsModel.ts b/packages/model-typings/src/models/ISettingsModel.ts index 9dc2005867fa6..d382d4853a4b7 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 aebda87c78cbe..56119982afb33 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'; @@ -121,6 +131,7 @@ export interface ISubscriptionsModel extends IBaseModel { findByUserId(userId: string, options?: FindOptions): FindCursor; cachedFindByUserId(userId: string, options?: FindOptions): FindCursor; updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise; + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise; findAlwaysNotifyDesktopUsersByRoomId(roomId: string): FindCursor; findOneByRoomNameAndUserId(roomName: string, userId: string): Promise; @@ -216,6 +227,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 f8eda6a3639bd..f14f5bc90d0d7 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; @@ -258,6 +259,7 @@ export interface IUsersModel extends IBaseModel { getNextAgent(ignoreAgentId?: string, extraQuery?: Filter): Promise<{ agentId: string; username: string } | null>; getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>; setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise; + makeAgentUnavailableAndUnsetExtension(userId: string): Promise; setLivechatData(userId: string, data?: Record): Promise; closeOffice(): Promise; openOffice(): Promise; @@ -316,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; @@ -371,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 c8249a587d150..ac261acbc4d7a 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,87 @@ # @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 + +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + ## 0.0.9 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 0a80a2975ffc1..1644a8362c771 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.0.9", + "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 e1cf91f1b0ee7..1e83fe72b93e8 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 0000000000000..15f2cd4817e1e --- /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 0000000000000..959a31a7c6bfc --- /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 0000000000000..52fa766671dbd --- /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 99dc0d6fabf59..8212df18002c2 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 0000000000000..ea58d0e83b079 --- /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 0000000000000..ce94042e029a0 --- /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 0000000000000..4cda16e3dd279 --- /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 0000000000000..f0a66c843c50e --- /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 b9d556c6ac074..8c9f239413ca8 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 ee73fe1526365..3435ec55ffa5e 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 769795a562406..0a1e3ded8927d 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 9c5aa19f1b077..da3d484645468 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,122 @@ # @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 + +- @rocket.chat/core-typings@6.3.4 + ## 6.3.3 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index acb2cb6354146..d5996987acf1f 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.3", + "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 b3aa5d3aa535f..0be60fc4413b0 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 066e3248dc339..3b8197ce20bf6 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 3690d0672ce27..914739d000a04 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 c8bb88cfc1c6c..e25dfb0ce2fb3 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 a63d37da07ba8..a6009fe20d854 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 c34a720bd4b76..7c3781d787c41 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 e9ef650656cde..249a12096729c 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 c6d102a967e4d..87c0106f6d3f9 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 4af37334e287d..804b72a763de1 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 69b1d85f22a5e..48b859a7899b9 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 f85358c38ee99..bebea2856861f 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/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 947228476bdd2..c47f4be6404d2 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -9,7 +9,6 @@ import type { } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; -import type { UsersSendConfirmationEmailParamsPOST } from '..'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST'; @@ -20,7 +19,9 @@ import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusP import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET'; import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet'; import type { UsersListTeamsParamsGET } from './users/UsersListTeamsParamsGET'; +import type { UsersSendConfirmationEmailParamsPOST } from './users/UsersSendConfirmationEmailParamsPOST'; import type { UsersSetPreferencesParamsPOST } from './users/UsersSetPreferenceParamsPOST'; +import type { UsersUpdateOwnBasicInfoParamsPOST } from './users/UsersUpdateOwnBasicInfoParamsPOST'; import type { UsersUpdateParamsPOST } from './users/UsersUpdateParamsPOST'; const ajv = new Ajv({ @@ -358,18 +359,7 @@ export type UsersEndpoints = { }; '/v1/users.updateOwnBasicInfo': { - POST: (params: { - data: { - email?: string; - name?: string; - username?: string; - nickname?: string; - statusText?: string; - newPassword?: string; - currentPassword?: string; - }; - customFields?: Record; - }) => { + POST: (params: UsersUpdateOwnBasicInfoParamsPOST) => { user: IUser; }; }; diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index b572a5bdc5321..bb32dc27fb04e 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -50,6 +50,7 @@ export type UsersSetPreferencesParamsPOST = { omnichannelTranscriptEmail?: boolean; omnichannelTranscriptPDF?: boolean; enableMobileRinging?: boolean; + mentionsWithSymbol?: boolean; }; }; @@ -245,6 +246,10 @@ const UsersSetPreferencesParamsPostSchema = { type: 'boolean', nullable: true, }, + mentionsWithSymbol: { + type: 'boolean', + nullable: true, + }, }, required: [], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts index cff6fee56bfaf..13c3066e4767b 100644 --- a/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts @@ -10,7 +10,9 @@ export type UsersUpdateOwnBasicInfoParamsPOST = { name?: string; username?: string; nickname?: string; + bio?: string; statusText?: string; + statusType?: string; currentPassword?: string; newPassword?: string; }; @@ -39,6 +41,14 @@ const UsersUpdateOwnBasicInfoParamsPostSchema = { type: 'string', nullable: true, }, + bio: { + type: 'string', + nullable: true, + }, + statusType: { + type: 'string', + nullable: true, + }, statusText: { type: 'string', nullable: true, diff --git a/packages/server-cloud-communication/.eslintrc.json b/packages/server-cloud-communication/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /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 0000000000000..52a3ff801dac8 --- /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 0000000000000..d554aa5380594 --- /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 0000000000000..382400b0c72ce --- /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 0000000000000..e2be47cf5499f --- /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/server-fetch/src/parsers.ts b/packages/server-fetch/src/parsers.ts index 598ecbbd0e8e4..ad0a44e96cfb8 100644 --- a/packages/server-fetch/src/parsers.ts +++ b/packages/server-fetch/src/parsers.ts @@ -1,32 +1,20 @@ import type { ExtendedFetchOptions, FetchOptions, OriginalFetchOptions } from './types'; -function isPostOrPutOrDeleteWithBody(options?: ExtendedFetchOptions): boolean { - // No method === 'get' - if (!options?.method) { - return false; - } - const { method, body } = options; - const lowerMethod = method?.toLowerCase(); - return ['post', 'put', 'delete'].includes(lowerMethod) && body != null; -} - const jsonParser = (options: ExtendedFetchOptions) => { if (!options) { return {}; } - if (isPostOrPutOrDeleteWithBody(options)) { - try { - if (options && typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { - options.body = JSON.stringify(options.body); - options.headers = { - 'Content-Type': 'application/json', - ...options.headers, - }; - } - } catch (e) { - // Body is not JSON, do nothing + try { + if (typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { + options.body = JSON.stringify(options.body); + options.headers = { + ...options.headers, + 'Content-Type': 'application/json', // force content type to be json + }; } + } catch (e) { + // Body is not JSON, do nothing } return options as FetchOptions; diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md new file mode 100644 index 0000000000000..b5d9e6f419d49 --- /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 d9ab70550ce4d..ed5e7bdd44bf1 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 261823100d0af..b8bc90d9cb549 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 0000000000000..bd830a92bfebd --- /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 497a40137ee57..77ab6ff504ca2 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,93 @@ # @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 + +- @rocket.chat/ui-contexts@1.0.4 + ## 1.0.3 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 4bc9a7d8c681e..f8ef2d3a1e93f 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.3", + "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.34.0", + "@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.3", + "@rocket.chat/ui-contexts": "2.0.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/src/components/Card/Card.stories.tsx b/packages/ui-client/src/components/Card/Card.stories.tsx index 4697443e9259e..56a2220219ed5 100644 --- a/packages/ui-client/src/components/Card/Card.stories.tsx +++ b/packages/ui-client/src/components/Card/Card.stories.tsx @@ -1,7 +1,7 @@ import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import Card from '.'; +import { Card, CardBody, CardCol, CardColSection, CardColTitle, CardDivider, CardFooter, CardIcon, CardTitle } from '.'; import TextSeparator from '../TextSeparator'; import { UserStatus } from '../UserStatus'; @@ -9,13 +9,13 @@ export default { title: 'Components/Card', component: Card, subcomponents: { - 'Card.Title': Card.Title, - 'Card.Body': Card.Body, - 'Card.Col': Card.Col, - 'Card.Col.Section': Card.Col.Section, - 'Card.Col.Title': Card.Col.Title, - 'Card.Footer': Card.Footer, - 'Card.Divider': Card.Divider, + CardTitle, + CardBody, + CardCol, + CardColSection, + CardColTitle, + CardFooter, + CardDivider, }, parameters: { layout: 'centered', @@ -25,14 +25,14 @@ export default { export const Example: ComponentStory = () => ( - Usage - - - Users + Usage + + + Users - Total + Total } value={123} @@ -40,9 +40,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Online } @@ -51,9 +51,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Busy } @@ -62,9 +62,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Away } @@ -73,111 +73,111 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Offline } value={123} /> - - - Types and Distribution + + + Types and Distribution - - - Uploads + + + Uploads - - + + ); export const Single: ComponentStory = () => ( - A card - - + A card + + - A Section + A Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
- Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
-
- + + + - +
); export const Double: ComponentStory = () => ( - A card - - + A card + + - A Section + A Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
- Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
- - + + + - A Section + A Section - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
-
- + + + - +
); diff --git a/packages/ui-client/src/components/Card/index.ts b/packages/ui-client/src/components/Card/index.ts index ade5391e61d6e..76df65e5a8098 100644 --- a/packages/ui-client/src/components/Card/index.ts +++ b/packages/ui-client/src/components/Card/index.ts @@ -1,30 +1,10 @@ -import Card from './Card'; -import CardBody from './CardBody'; -import CardCol from './CardCol'; -import CardColSection from './CardColSection'; -import CardColTitle from './CardColTitle'; -import CardDivider from './CardDivider'; -import CardFooter from './CardFooter'; -import CardFooterWrapper from './CardFooterWrapper'; -import CardIcon from './CardIcon'; -import CardTitle from './CardTitle'; - -export const DOUBLE_COLUMN_CARD_WIDTH = 552; - -/** - * @deprecated Avoid default usage, use named imports instead - */ -export default Object.assign(Card, { - Title: CardTitle, - Body: CardBody, - Col: Object.assign(CardCol, { - Title: CardColTitle, - Section: CardColSection, - }), - Footer: CardFooter, - FooterWrapper: CardFooterWrapper, - Divider: CardDivider, - Icon: CardIcon, -}); - -export { Card, CardBody, CardCol, CardColSection, CardColTitle, CardDivider, CardFooter, CardFooterWrapper, CardIcon, CardTitle }; +export { default as Card } from './Card'; +export { default as CardBody } from './CardBody'; +export { default as CardCol } from './CardCol'; +export { default as CardColSection } from './CardColSection'; +export { default as CardColTitle } from './CardColTitle'; +export { default as CardDivider } from './CardDivider'; +export { default as CardFooter } from './CardFooter'; +export { default as CardFooterWrapper } from './CardFooterWrapper'; +export { default as CardIcon } from './CardIcon'; +export { default as CardTitle } from './CardTitle'; diff --git a/packages/ui-client/src/components/CustomFieldsForm.tsx b/packages/ui-client/src/components/CustomFieldsForm.tsx index 9423456ebe8ed..0d1ba2ca5b1b4 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 1420a62346d63..6c5e12be86221 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 7e6bfdb9fee18..d8f8d60d80961 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)} ) : ( -