diff --git a/.changeset/blue-ladybugs-raise.md b/.changeset/blue-ladybugs-raise.md deleted file mode 100644 index 44d7a06b4111..000000000000 --- a/.changeset/blue-ladybugs-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Translation files are requested multiple times diff --git a/.changeset/breezy-bugs-jam.md b/.changeset/breezy-bugs-jam.md deleted file mode 100644 index 7e7cc7b8283b..000000000000 --- a/.changeset/breezy-bugs-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Managers allowed to make deactivated agent's available diff --git a/.changeset/bright-carpets-fly.md b/.changeset/bright-carpets-fly.md deleted file mode 100644 index 6a8ac2608569..000000000000 --- a/.changeset/bright-carpets-fly.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -new: ring mobile users on direct conference calls diff --git a/.changeset/bright-snakes-vanish.md b/.changeset/bright-snakes-vanish.md deleted file mode 100644 index f198bfe93ae9..000000000000 --- a/.changeset/bright-snakes-vanish.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue causing `queue time` to be calculated from current time when a room was closed without being served. -Now: -- For served rooms: queue time = servedBy time - queuedAt -- For not served, but open rooms = now - queuedAt -- For not served and closed rooms = closedAt - queuedAt diff --git a/.changeset/brown-clouds-add.md b/.changeset/brown-clouds-add.md deleted file mode 100644 index 6b69289177b5..000000000000 --- a/.changeset/brown-clouds-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Performance issue on `Messages.countByType` aggregation caused by unindexed property on messages collection diff --git a/.changeset/brown-comics-cheat.md b/.changeset/brown-comics-cheat.md new file mode 100644 index 000000000000..a7907979881b --- /dev/null +++ b/.changeset/brown-comics-cheat.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/model-typings": patch +--- + +chore: Calculate & Store MAC stats +Added new info to the stats: `omnichannelContactsBySource`, `uniqueContactsOfLastMonth`, `uniqueContactsOfLastWeek`, `uniqueContactsOfYesterday` diff --git a/.changeset/bump-patch-1694741499930.md b/.changeset/bump-patch-1694741499930.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1694741499930.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1694827499043.md b/.changeset/bump-patch-1694827499043.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1694827499043.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1695163548038.md b/.changeset/bump-patch-1695163548038.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1695163548038.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1695165575069.md b/.changeset/bump-patch-1695165575069.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1695165575069.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/chilled-flies-fold.md b/.changeset/chilled-flies-fold.md deleted file mode 100644 index 17a0f9eb6dc5..000000000000 --- a/.changeset/chilled-flies-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -feat: add sections to room header and user infos menus with menuV2 diff --git a/.changeset/chilled-phones-give.md b/.changeset/chilled-phones-give.md deleted file mode 100644 index cb0887db0883..000000000000 --- a/.changeset/chilled-phones-give.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch -"@rocket.chat/rest-typings": patch ---- - -Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints diff --git a/.changeset/cool-students-tan.md b/.changeset/cool-students-tan.md deleted file mode 100644 index 07760541628a..000000000000 --- a/.changeset/cool-students-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -feat(apps): `ActionManagerBusyState` component for apps `ui.interaction` diff --git a/.changeset/cuddly-houses-tie.md b/.changeset/cuddly-houses-tie.md deleted file mode 100644 index 76d86a690388..000000000000 --- a/.changeset/cuddly-houses-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Hide Reset TOTP option if 2FA is disabled diff --git a/.changeset/cuddly-ties-bake.md b/.changeset/cuddly-ties-bake.md deleted file mode 100644 index d912d2969d75..000000000000 --- a/.changeset/cuddly-ties-bake.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": minor -"@rocket.chat/uikit-playground": minor ---- - -feat: Add missing variants to UIKit button diff --git a/.changeset/curly-shoes-burn.md b/.changeset/curly-shoes-burn.md deleted file mode 100644 index 67d453ab7245..000000000000 --- a/.changeset/curly-shoes-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Added ability to freeze or completely disable integration scripts through envvars diff --git a/.changeset/custom-emoji-fs.md b/.changeset/custom-emoji-fs.md deleted file mode 100644 index a9a797f35bc8..000000000000 --- a/.changeset/custom-emoji-fs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: custom emoji upload with FileSystem method diff --git a/.changeset/dropdown.md b/.changeset/dropdown.md deleted file mode 100644 index 935c12aebe85..000000000000 --- a/.changeset/dropdown.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -New filters to the Rooms Table at `Workspace > Rooms` diff --git a/.changeset/dull-trainers-drive.md b/.changeset/dull-trainers-drive.md new file mode 100644 index 000000000000..f5a673cd8c30 --- /dev/null +++ b/.changeset/dull-trainers-drive.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Remove model-level query restrictions for monitors diff --git a/.changeset/eighty-kids-jog.md b/.changeset/eighty-kids-jog.md deleted file mode 100644 index 6410813d80a6..000000000000 --- a/.changeset/eighty-kids-jog.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/livechat': minor -'@rocket.chat/meteor': minor ---- - -Added new Omnichannel's trigger condition "After starting a chat". diff --git a/.changeset/eleven-icons-tan.md b/.changeset/eleven-icons-tan.md deleted file mode 100644 index c51124c05dd2..000000000000 --- a/.changeset/eleven-icons-tan.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-typings": minor -"@rocket.chat/model-typings": minor -"@rocket.chat/rest-typings": minor ---- - -Introduce the ability to report an user diff --git a/.changeset/empty-ants-enjoy.md b/.changeset/empty-ants-enjoy.md deleted file mode 100644 index 4a55f82d0abf..000000000000 --- a/.changeset/empty-ants-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -fix: Wrong toast message while creating a new custom sound with an existing name diff --git a/.changeset/fair-cats-destroy.md b/.changeset/fair-cats-destroy.md deleted file mode 100644 index 7dfb74955a94..000000000000 --- a/.changeset/fair-cats-destroy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -When setting a room as read-only, do not allow previously unmuted users to send messages. diff --git a/.changeset/fast-pumpkins-smoke.md b/.changeset/fast-pumpkins-smoke.md deleted file mode 100644 index 2374776bf3b5..000000000000 --- a/.changeset/fast-pumpkins-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fix: finnish translation diff --git a/.changeset/fast-yaks-collect.md b/.changeset/fast-yaks-collect.md deleted file mode 100644 index 60dd92030163..000000000000 --- a/.changeset/fast-yaks-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue where 2fa was not working after an OAuth redirect diff --git a/.changeset/fifty-cars-divide.md b/.changeset/fifty-cars-divide.md deleted file mode 100644 index 6c09cf6869c8..000000000000 --- a/.changeset/fifty-cars-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed issue with custom OAuth services' settings not being be fully removed diff --git a/.changeset/fluffy-beds-buy.md b/.changeset/fluffy-beds-buy.md deleted file mode 100644 index f90513b946c3..000000000000 --- a/.changeset/fluffy-beds-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Added support for threaded conversation in Federated rooms. diff --git a/.changeset/fluffy-lions-rage.md b/.changeset/fluffy-lions-rage.md deleted file mode 100644 index 09437a2cb88e..000000000000 --- a/.changeset/fluffy-lions-rage.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -chore: (Livechat) Replace all `dangerouslySetInnerHTML` with `gazzodown` diff --git a/.changeset/forty-hotels-pretend.md b/.changeset/forty-hotels-pretend.md deleted file mode 100644 index b23825d5a02a..000000000000 --- a/.changeset/forty-hotels-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times diff --git a/.changeset/four-parents-cheer.md b/.changeset/four-parents-cheer.md deleted file mode 100644 index 2fbb8e2b279f..000000000000 --- a/.changeset/four-parents-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -load sounds right before playing them diff --git a/.changeset/friendly-glasses-mate.md b/.changeset/friendly-glasses-mate.md deleted file mode 100644 index 6a7a7b4f8546..000000000000 --- a/.changeset/friendly-glasses-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -fix: Time format of Retention Policy diff --git a/.changeset/fuzzy-glasses-divide.md b/.changeset/fuzzy-glasses-divide.md deleted file mode 100644 index cf77bbde5507..000000000000 --- a/.changeset/fuzzy-glasses-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Added a new Roles bridge to RC Apps-Engine for reading and retrieving role details. diff --git a/.changeset/fuzzy-schools-brake.md b/.changeset/fuzzy-schools-brake.md deleted file mode 100644 index c6af54a2ef57..000000000000 --- a/.changeset/fuzzy-schools-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix users being created without the `roles` field diff --git a/.changeset/gold-horses-pretend.md b/.changeset/gold-horses-pretend.md deleted file mode 100644 index a8908b68a23e..000000000000 --- a/.changeset/gold-horses-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed CAS login after popup closes diff --git a/.changeset/gold-moose-press.md b/.changeset/gold-moose-press.md deleted file mode 100644 index 605fb7c649ea..000000000000 --- a/.changeset/gold-moose-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix moment timestamps language change diff --git a/.changeset/good-elephants-live.md b/.changeset/good-elephants-live.md deleted file mode 100644 index 8cb3e9d87fc4..000000000000 --- a/.changeset/good-elephants-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed message fetching method in LivechatBridge for Apps diff --git a/.changeset/green-adults-peel.md b/.changeset/green-adults-peel.md deleted file mode 100644 index b07f5ea3e6bf..000000000000 --- a/.changeset/green-adults-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix pruning messages in a room results in an incorrect message counter diff --git a/.changeset/grumpy-candles-rule.md b/.changeset/grumpy-candles-rule.md deleted file mode 100644 index 28673ce91a73..000000000000 --- a/.changeset/grumpy-candles-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fix: rejected conference calls continue to ring diff --git a/.changeset/heavy-baboons-laugh.md b/.changeset/heavy-baboons-laugh.md deleted file mode 100644 index 5c32965dcf62..000000000000 --- a/.changeset/heavy-baboons-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -User information crashing for some locales diff --git a/.changeset/heavy-cougars-marry.md b/.changeset/heavy-cougars-marry.md deleted file mode 100644 index 893f53352114..000000000000 --- a/.changeset/heavy-cougars-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix performance issue on Engagement Dashboard aggregation diff --git a/.changeset/heavy-zebras-wonder.md b/.changeset/heavy-zebras-wonder.md deleted file mode 100644 index a1904a81c514..000000000000 --- a/.changeset/heavy-zebras-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Show correct date for last day time diff --git a/.changeset/hip-hounds-ring.md b/.changeset/hip-hounds-ring.md deleted file mode 100644 index 79dfba6dd031..000000000000 --- a/.changeset/hip-hounds-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Added ability to disable private app installation via envvar (DISABLE_PRIVATE_APP_INSTALLATION) diff --git a/.changeset/hip-mugs-promise.md b/.changeset/hip-mugs-promise.md deleted file mode 100644 index 7100fec026e3..000000000000 --- a/.changeset/hip-mugs-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -improve: System messages for omni-visitor abandonment feature diff --git a/.changeset/honest-glasses-roll.md b/.changeset/honest-glasses-roll.md deleted file mode 100644 index 679f46fb8420..000000000000 --- a/.changeset/honest-glasses-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -chore: Add danger variant to apps action button menus diff --git a/.changeset/honest-mirrors-sit.md b/.changeset/honest-mirrors-sit.md deleted file mode 100644 index 4e4298cb8110..000000000000 --- a/.changeset/honest-mirrors-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Disabled call to tags enterprise endpoint when on community license diff --git a/.changeset/honest-numbers-compete.md b/.changeset/honest-numbers-compete.md deleted file mode 100644 index 1fd017e7fc16..000000000000 --- a/.changeset/honest-numbers-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes SAML full name updates not being mirrored to DM rooms. diff --git a/.changeset/importer-progress-bar.md b/.changeset/importer-progress-bar.md deleted file mode 100644 index 49c04289ddcb..000000000000 --- a/.changeset/importer-progress-bar.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed Importer Progress Bar progress indicator - diff --git a/.changeset/khaki-feet-dance.md b/.changeset/khaki-feet-dance.md new file mode 100644 index 000000000000..a419afa34143 --- /dev/null +++ b/.changeset/khaki-feet-dance.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +feat: Save visitor's activity on agent's interaction diff --git a/.changeset/kind-students-worry.md b/.changeset/kind-students-worry.md deleted file mode 100644 index 554c1c1204ea..000000000000 --- a/.changeset/kind-students-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Make user default role setting public diff --git a/.changeset/lazy-ghosts-design.md b/.changeset/lazy-ghosts-design.md deleted file mode 100644 index 080e9986cebb..000000000000 --- a/.changeset/lazy-ghosts-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed misleading of 'total' in team members list inside Channel diff --git a/.changeset/loud-sheep-try.md b/.changeset/loud-sheep-try.md deleted file mode 100644 index f82d0d069554..000000000000 --- a/.changeset/loud-sheep-try.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": minor -"@rocket.chat/uikit-playground": minor ---- - -feat: Adding new UIKit components: Callout, Checkbox, Radio Button, Time Picker, Toast Bar, Toggle Switch, Tab Navigation diff --git a/.changeset/lovely-snails-drop.md b/.changeset/lovely-snails-drop.md deleted file mode 100644 index 4e28c6a43c20..000000000000 --- a/.changeset/lovely-snails-drop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -Fix spotlight search does not find rooms with special or non-latin characters diff --git a/.changeset/lucky-balloons-divide.md b/.changeset/lucky-balloons-divide.md deleted file mode 100644 index beb4cbfe3b57..000000000000 --- a/.changeset/lucky-balloons-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix engagement dashboard not showing data diff --git a/.changeset/lucky-hounds-sing.md b/.changeset/lucky-hounds-sing.md deleted file mode 100644 index 20b09afaf545..000000000000 --- a/.changeset/lucky-hounds-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed wrong user status displayed during mentioning a user in a channel diff --git a/.changeset/many-icons-provide.md b/.changeset/many-icons-provide.md deleted file mode 100644 index bf82407980ad..000000000000 --- a/.changeset/many-icons-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Don't allow to report self messages diff --git a/.changeset/mighty-walls-smash.md b/.changeset/mighty-walls-smash.md deleted file mode 100644 index 54b2846901de..000000000000 --- a/.changeset/mighty-walls-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed scrollbar over content in Federated Room List diff --git a/.changeset/moody-comics-cheat.md b/.changeset/moody-comics-cheat.md deleted file mode 100644 index b8b372306d0e..000000000000 --- a/.changeset/moody-comics-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/release-action': minor ---- - -Add back "Engine Versions" to the release notes diff --git a/.changeset/moody-pans-act.md b/.changeset/moody-pans-act.md deleted file mode 100644 index 6c307604eaa9..000000000000 --- a/.changeset/moody-pans-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix seat counter including bots users diff --git a/.changeset/nine-bottles-press.md b/.changeset/nine-bottles-press.md deleted file mode 100644 index f9a57fa676ad..000000000000 --- a/.changeset/nine-bottles-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -feat: Add flag to disable teams mention via troubleshoot page diff --git a/.changeset/nine-carrots-listen.md b/.changeset/nine-carrots-listen.md deleted file mode 100644 index bf5dc72e6cc0..000000000000 --- a/.changeset/nine-carrots-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed layout changing from embedded view when navigating diff --git a/.changeset/odd-elephants-promise.md b/.changeset/odd-elephants-promise.md deleted file mode 100644 index a12817ed175b..000000000000 --- a/.changeset/odd-elephants-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix LinkedIn OAuth broken diff --git a/.changeset/old-federation-card.md b/.changeset/old-federation-card.md deleted file mode 100644 index fa9879d84426..000000000000 --- a/.changeset/old-federation-card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Removed old/deprecated Rocket.Chat Federation card from Info page diff --git a/.changeset/perfect-adults-travel.md b/.changeset/perfect-adults-travel.md deleted file mode 100644 index 61ae4ab6dad5..000000000000 --- a/.changeset/perfect-adults-travel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/fuselage-ui-kit": patch -"@rocket.chat/uikit-playground": patch ---- - -feat(fuselage-ui-kit): Introduce `TabsNavigationBlock` diff --git a/.changeset/pink-zoos-join.md b/.changeset/pink-zoos-join.md deleted file mode 100644 index dcc1088de0b5..000000000000 --- a/.changeset/pink-zoos-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix the code that was setting email URL to an invalid value when SMTP was not set diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 3bc535a4b50f..000000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@rocket.chat/meteor": "6.4.0-develop", - "rocketchat-services": "1.1.4", - "@rocket.chat/account-service": "0.2.4", - "@rocket.chat/authorization-service": "0.2.4", - "@rocket.chat/ddp-streamer": "0.1.4", - "@rocket.chat/omnichannel-transcript": "0.2.4", - "@rocket.chat/presence-service": "0.2.4", - "@rocket.chat/queue-worker": "0.2.4", - "@rocket.chat/stream-hub-service": "0.2.4", - "@rocket.chat/api-client": "0.1.4", - "@rocket.chat/ddp-client": "0.1.4", - "@rocket.chat/omnichannel-services": "0.0.10", - "@rocket.chat/pdf-worker": "0.0.10", - "@rocket.chat/presence": "0.0.10", - "@rocket.chat/ui-theming": "0.0.1", - "@rocket.chat/account-utils": "0.0.1", - "@rocket.chat/agenda": "0.0.2", - "@rocket.chat/base64": "1.0.12", - "@rocket.chat/cas-validate": "0.0.1", - "@rocket.chat/core-services": "0.1.4", - "@rocket.chat/core-typings": "6.3.4", - "@rocket.chat/cron": "0.0.6", - "@rocket.chat/eslint-config": "0.5.2", - "@rocket.chat/favicon": "0.0.1", - "@rocket.chat/fuselage-ui-kit": "1.0.4", - "@rocket.chat/gazzodown": "1.0.4", - "@rocket.chat/i18n": "0.0.1", - "@rocket.chat/instance-status": "0.0.10", - "@rocket.chat/livechat": "1.13.4", - "@rocket.chat/log-format": "0.0.1", - "@rocket.chat/logger": "0.0.1", - "@rocket.chat/mock-providers": "0.0.1", - "@rocket.chat/model-typings": "0.0.10", - "@rocket.chat/models": "0.0.10", - "@rocket.chat/poplib": "0.0.1", - "@rocket.chat/random": "1.2.1", - "@rocket.chat/release-action": "2.1.0", - "@rocket.chat/rest-typings": "6.3.4", - "@rocket.chat/server-fetch": "0.0.1", - "@rocket.chat/sha256": "1.0.9", - "@rocket.chat/tools": "0.0.1", - "@rocket.chat/ui-client": "1.0.4", - "@rocket.chat/ui-composer": "0.0.1", - "@rocket.chat/ui-contexts": "1.0.4", - "@rocket.chat/ui-video-conf": "1.0.4", - "@rocket.chat/uikit-playground": "0.1.4", - "@rocket.chat/web-ui-registration": "1.0.4" - }, - "changesets": [ - "blue-ladybugs-raise", - "breezy-bugs-jam", - "bright-carpets-fly", - "bright-snakes-vanish", - "brown-clouds-add", - "bump-patch-1694741499930", - "bump-patch-1694827499043", - "bump-patch-1695163548038", - "bump-patch-1695165575069", - "chilled-flies-fold", - "chilled-phones-give", - "cool-students-tan", - "cuddly-houses-tie", - "cuddly-ties-bake", - "curly-shoes-burn", - "custom-emoji-fs", - "dropdown", - "eighty-kids-jog", - "eleven-icons-tan", - "empty-ants-enjoy", - "fair-cats-destroy", - "fast-pumpkins-smoke", - "fast-yaks-collect", - "fifty-cars-divide", - "fluffy-beds-buy", - "fluffy-lions-rage", - "forty-hotels-pretend", - "four-parents-cheer", - "friendly-glasses-mate", - "fuzzy-glasses-divide", - "fuzzy-schools-brake", - "gold-horses-pretend", - "gold-moose-press", - "good-elephants-live", - "green-adults-peel", - "grumpy-candles-rule", - "heavy-baboons-laugh", - "heavy-cougars-marry", - "heavy-zebras-wonder", - "hip-hounds-ring", - "hip-mugs-promise", - "honest-glasses-roll", - "honest-mirrors-sit", - "honest-numbers-compete", - "importer-progress-bar", - "kind-students-worry", - "lazy-ghosts-design", - "loud-sheep-try", - "lovely-snails-drop", - "lucky-balloons-divide", - "lucky-hounds-sing", - "many-icons-provide", - "mighty-walls-smash", - "moody-comics-cheat", - "moody-pans-act", - "nine-bottles-press", - "nine-carrots-listen", - "odd-elephants-promise", - "old-federation-card", - "perfect-adults-travel", - "pink-zoos-join", - "pretty-bees-give", - "quick-emus-march", - "quiet-phones-sell", - "rare-sheep-yawn", - "real-pets-visit", - "red-windows-admire", - "red-zebras-clap", - "rotten-turtles-agree", - "serious-garlics-clean", - "serious-geckos-drive", - "serious-shrimps-try", - "seven-jobs-tickle", - "shaggy-beans-poke", - "shiny-garlics-carry", - "shiny-tools-worry", - "short-cobras-tell", - "silly-actors-laugh", - "silver-mugs-unite", - "six-buckets-eat", - "slimy-cheetahs-heal", - "slimy-wasps-double", - "slow-lizards-breathe", - "small-rice-repair", - "smooth-planes-cough", - "soft-yaks-matter", - "sour-cows-refuse", - "sour-parrots-nail", - "stale-roses-knock", - "strong-laws-pump", - "swift-birds-build", - "swift-walls-protect", - "tall-pumpkins-cross", - "tame-pens-occur", - "three-ants-give", - "three-birds-tickle", - "tidy-bears-camp", - "tiny-turkeys-burn", - "tough-candles-heal", - "tricky-years-swim", - "unlucky-turtles-search", - "user-mention", - "violet-frogs-cheer", - "warm-hornets-ring", - "wet-frogs-kiss", - "wet-walls-lie", - "wild-spiders-smell", - "wise-onions-trade", - "wise-walls-tan", - "wise-ways-fetch", - "witty-feet-warn", - "yellow-buttons-agree", - "yellow-schools-tell", - "young-trains-glow" - ] -} diff --git a/.changeset/pretty-bees-give.md b/.changeset/pretty-bees-give.md deleted file mode 100644 index 8891420308c5..000000000000 --- a/.changeset/pretty-bees-give.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor ---- - -Add option to select what URL previews should be generated for each message. diff --git a/.changeset/quick-emus-march.md b/.changeset/quick-emus-march.md deleted file mode 100644 index 7a6d7b444654..000000000000 --- a/.changeset/quick-emus-march.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/ddp-client': minor -'@rocket.chat/core-services': minor -'@rocket.chat/meteor': minor ---- - -Add new event to notify users directly about new banners diff --git a/.changeset/quiet-phones-sell.md b/.changeset/quiet-phones-sell.md deleted file mode 100644 index a6222cba16c9..000000000000 --- a/.changeset/quiet-phones-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue where oauth login was not working with some providers diff --git a/.changeset/rare-sheep-yawn.md b/.changeset/rare-sheep-yawn.md deleted file mode 100644 index 86c2d7283223..000000000000 --- a/.changeset/rare-sheep-yawn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -fix: Issue caused by spaces in the `config.url` setting diff --git a/.changeset/real-pets-visit.md b/.changeset/real-pets-visit.md deleted file mode 100644 index d6531285597c..000000000000 --- a/.changeset/real-pets-visit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch ---- - -Fixed `default` field not being returned from the `setDefault` endpoints when setting to false diff --git a/.changeset/red-windows-admire.md b/.changeset/red-windows-admire.md deleted file mode 100644 index 48a82b5902cb..000000000000 --- a/.changeset/red-windows-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed an issue where timeout for http requests in Apps-Engine bridges was too short diff --git a/.changeset/red-zebras-clap.md b/.changeset/red-zebras-clap.md deleted file mode 100644 index cd8f832b1835..000000000000 --- a/.changeset/red-zebras-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix importer filters not working diff --git a/.changeset/rotten-turtles-agree.md b/.changeset/rotten-turtles-agree.md deleted file mode 100644 index f915aa38f758..000000000000 --- a/.changeset/rotten-turtles-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: stop blinking "Room not found" before dm creation diff --git a/.changeset/selfish-hounds-pay.md b/.changeset/selfish-hounds-pay.md new file mode 100644 index 000000000000..3ca321bd392f --- /dev/null +++ b/.changeset/selfish-hounds-pay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Monitors now able to forward a chat without taking it first diff --git a/.changeset/serious-garlics-clean.md b/.changeset/serious-garlics-clean.md deleted file mode 100644 index ccdc3c94dda4..000000000000 --- a/.changeset/serious-garlics-clean.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/core-services': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -New AddUser workflow for Federated Rooms diff --git a/.changeset/serious-geckos-drive.md b/.changeset/serious-geckos-drive.md deleted file mode 100644 index 454337399772..000000000000 --- 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 114293aa104e..000000000000 --- a/.changeset/serious-shrimps-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed an issue with the positioning of the message menu diff --git a/.changeset/seven-carpets-march.md b/.changeset/seven-carpets-march.md new file mode 100644 index 000000000000..46fd1b7ddb62 --- /dev/null +++ b/.changeset/seven-carpets-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Add new permission to allow kick users from rooms without being a member diff --git a/.changeset/seven-jobs-tickle.md b/.changeset/seven-jobs-tickle.md deleted file mode 100644 index 870bafbb7d9d..000000000000 --- a/.changeset/seven-jobs-tickle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/model-typings": patch ---- - -fix: agent role being removed upon user deactivation diff --git a/.changeset/shaggy-beans-poke.md b/.changeset/shaggy-beans-poke.md deleted file mode 100644 index 31a480638952..000000000000 --- a/.changeset/shaggy-beans-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fix `mention-here` and `mention-all` permissions not being honored diff --git a/.changeset/shiny-garlics-carry.md b/.changeset/shiny-garlics-carry.md deleted file mode 100644 index 117063d93f6f..000000000000 --- a/.changeset/shiny-garlics-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix CORS headers not being set for assets diff --git a/.changeset/shiny-tools-worry.md b/.changeset/shiny-tools-worry.md deleted file mode 100644 index f024eca38d04..000000000000 --- a/.changeset/shiny-tools-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -feat: remove enforce password fallback dependency diff --git a/.changeset/short-cobras-tell.md b/.changeset/short-cobras-tell.md deleted file mode 100644 index 1c28ce7bad11..000000000000 --- a/.changeset/short-cobras-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Reorganized the message menu diff --git a/.changeset/silly-actors-laugh.md b/.changeset/silly-actors-laugh.md deleted file mode 100644 index aab23e14e5f1..000000000000 --- a/.changeset/silly-actors-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed Slackbridge was not handling correctly received events from Slack anymore. Events: (Send, edit, delete, react meassages) diff --git a/.changeset/silver-mugs-unite.md b/.changeset/silver-mugs-unite.md deleted file mode 100644 index be74b1bef215..000000000000 --- a/.changeset/silver-mugs-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: show requested filters only on requested apps view diff --git a/.changeset/six-buckets-eat.md b/.changeset/six-buckets-eat.md deleted file mode 100644 index f99bcfb71c30..000000000000 --- a/.changeset/six-buckets-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix users not able to login after block time perdiod has passed diff --git a/.changeset/slimy-cheetahs-heal.md b/.changeset/slimy-cheetahs-heal.md deleted file mode 100644 index 44233bc87766..000000000000 --- a/.changeset/slimy-cheetahs-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed selected departments not being displayed due to pagination diff --git a/.changeset/slimy-wasps-double.md b/.changeset/slimy-wasps-double.md deleted file mode 100644 index b28de342b274..000000000000 --- a/.changeset/slimy-wasps-double.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/meteor': minor -'@rocket.chat/ui-contexts': minor ---- - -UX improvement for the Moderation Console Context bar for viewing the reported messages. The Report reason is now displayed in the reported messages context bar. -The Moderation Action Modal confirmation description is updated to be more clear and concise. diff --git a/.changeset/slow-lizards-breathe.md b/.changeset/slow-lizards-breathe.md deleted file mode 100644 index fd773b17f5c8..000000000000 --- a/.changeset/slow-lizards-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed Apps-Engine event `IPostUserCreated` execution diff --git a/.changeset/small-rice-repair.md b/.changeset/small-rice-repair.md deleted file mode 100644 index 67fdff5ca758..000000000000 --- a/.changeset/small-rice-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix validation in app status call that allowed Enterprise apps to be enabled in invalid environments diff --git a/.changeset/smooth-planes-cough.md b/.changeset/smooth-planes-cough.md deleted file mode 100644 index 9ad4239f0342..000000000000 --- a/.changeset/smooth-planes-cough.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/ui-theming': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -feat: high-contrast theme diff --git a/.changeset/soft-yaks-matter.md b/.changeset/soft-yaks-matter.md deleted file mode 100644 index c326eb7dca70..000000000000 --- a/.changeset/soft-yaks-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -feat: return all broken password policies at once diff --git a/.changeset/sour-cows-refuse.md b/.changeset/sour-cows-refuse.md deleted file mode 100644 index d907c063f568..000000000000 --- a/.changeset/sour-cows-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed inviter not informed when inviting member to room via `/invite` slashcommand diff --git a/.changeset/sour-parrots-nail.md b/.changeset/sour-parrots-nail.md deleted file mode 100644 index 1c1eaa3173a8..000000000000 --- a/.changeset/sour-parrots-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed "teams" icon not being displayed on spotlight sidebar search diff --git a/.changeset/stale-roses-knock.md b/.changeset/stale-roses-knock.md deleted file mode 100644 index 25e93fa8c346..000000000000 --- a/.changeset/stale-roses-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: User timezone not being respected on Current Chat's filter diff --git a/.changeset/strong-laws-pump.md b/.changeset/strong-laws-pump.md deleted file mode 100644 index a4afefd65316..000000000000 --- a/.changeset/strong-laws-pump.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@rocket.chat/model-typings': patch -'@rocket.chat/meteor': patch ---- - -Change SAU aggregation to consider only sessions from few days ago instead of the whole past. - -This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. diff --git a/.changeset/sweet-feet-relate.md b/.changeset/sweet-feet-relate.md new file mode 100644 index 000000000000..f7da740ebcc0 --- /dev/null +++ b/.changeset/sweet-feet-relate.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: user dropdown menu position on RTL layout diff --git a/.changeset/swift-birds-build.md b/.changeset/swift-birds-build.md deleted file mode 100644 index 4af3bddd875b..000000000000 --- a/.changeset/swift-birds-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed unable to create admin user using ADMIN\_\* environment variables diff --git a/.changeset/swift-walls-protect.md b/.changeset/swift-walls-protect.md deleted file mode 100644 index 6e3057775c32..000000000000 --- a/.changeset/swift-walls-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed failing user data exports diff --git a/.changeset/tall-pumpkins-cross.md b/.changeset/tall-pumpkins-cross.md deleted file mode 100644 index e6cfd8a309b9..000000000000 --- a/.changeset/tall-pumpkins-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/presence": patch ---- - -Fixed presence broadcast being disabled on server restart diff --git a/.changeset/tame-pens-occur.md b/.changeset/tame-pens-occur.md deleted file mode 100644 index 8cb729531fae..000000000000 --- a/.changeset/tame-pens-occur.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/model-typings": minor ---- - -Fixed read receipts not getting deleted after corresponding message is deleted \ No newline at end of file diff --git a/.changeset/three-ants-give.md b/.changeset/three-ants-give.md deleted file mode 100644 index 4d33fad05f39..000000000000 --- a/.changeset/three-ants-give.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/cron": patch -"@rocket.chat/meteor": patch ---- - -Increase cron job check delay to 1 min from 5s. - -This reduces MongoDB requests introduced on 6.3. diff --git a/.changeset/three-birds-tickle.md b/.changeset/three-birds-tickle.md deleted file mode 100644 index 0ce911d9f6fa..000000000000 --- a/.changeset/three-birds-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. diff --git a/.changeset/tidy-bears-applaud.md b/.changeset/tidy-bears-applaud.md new file mode 100644 index 000000000000..cff12f3dc7d3 --- /dev/null +++ b/.changeset/tidy-bears-applaud.md @@ -0,0 +1,10 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor +--- + +Create a deployment fingerprint to identify possible deployment changes caused by database cloning. A question to the admin will confirm if it's a regular deployment change or an intent of a new deployment and correct identification values as needed. +The fingerprint is composed by `${siteUrl}${dbConnectionString}` and hashed via `sha256` in `base64`. +An environment variable named `AUTO_ACCEPT_FINGERPRINT`, when set to `true`, can be used to auto-accept an expected fingerprint change as a regular deployment update. diff --git a/.changeset/tidy-bears-camp.md b/.changeset/tidy-bears-camp.md deleted file mode 100644 index 3c2013f79023..000000000000 --- a/.changeset/tidy-bears-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Introduced upsells for the engagement dashboard and device management admin sidebar items in CE workspaces. Additionally, restructured the admin sidebar items to enhance organization. diff --git a/.changeset/tiny-turkeys-burn.md b/.changeset/tiny-turkeys-burn.md deleted file mode 100644 index a146bd6a0eae..000000000000 --- a/.changeset/tiny-turkeys-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -fixed an issue on oauth login that caused missing emails to be detected as changed data diff --git a/.changeset/tiny-wolves-deliver.md b/.changeset/tiny-wolves-deliver.md new file mode 100644 index 000000000000..f89564a9b53c --- /dev/null +++ b/.changeset/tiny-wolves-deliver.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/ui-theming': patch +--- + +fix: light-theme font-disabled color diff --git a/.changeset/tough-candles-heal.md b/.changeset/tough-candles-heal.md deleted file mode 100644 index 59ad9c1fb3a1..000000000000 --- a/.changeset/tough-candles-heal.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch -"@rocket.chat/model-typings": patch ---- - -Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. diff --git a/.changeset/tricky-years-swim.md b/.changeset/tricky-years-swim.md deleted file mode 100644 index 2ab1254525b2..000000000000 --- a/.changeset/tricky-years-swim.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/rest-typings": patch ---- - -Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data diff --git a/.changeset/unlucky-turtles-search.md b/.changeset/unlucky-turtles-search.md deleted file mode 100644 index fffa51020e30..000000000000 --- a/.changeset/unlucky-turtles-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed Accounts profile form name change was not working diff --git a/.changeset/user-mention.md b/.changeset/user-mention.md deleted file mode 100644 index a896a7c12ee4..000000000000 --- a/.changeset/user-mention.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed user mentioning when prepending the username with `>` diff --git a/.changeset/violet-frogs-cheer.md b/.changeset/violet-frogs-cheer.md deleted file mode 100644 index db48243c40ed..000000000000 --- a/.changeset/violet-frogs-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/fuselage-ui-kit': patch ---- - -Handle invalid context on `VideoConferenceBlock` component diff --git a/.changeset/warm-hornets-ring.md b/.changeset/warm-hornets-ring.md deleted file mode 100644 index f81cf1efbe92..000000000000 --- a/.changeset/warm-hornets-ring.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch ---- - -Use group filter when set to LDAP sync process diff --git a/.changeset/warm-melons-type.md b/.changeset/warm-melons-type.md new file mode 100644 index 000000000000..5b187b8a7f11 --- /dev/null +++ b/.changeset/warm-melons-type.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/omnichannel-services": patch +--- + +feat: Disable and annonimize visitors instead of removing diff --git a/.changeset/wet-frogs-kiss.md b/.changeset/wet-frogs-kiss.md deleted file mode 100644 index 24395a78f85d..000000000000 --- a/.changeset/wet-frogs-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fix: Missing padding on Omnichannel contacts Contextualbar loading state diff --git a/.changeset/wet-walls-lie.md b/.changeset/wet-walls-lie.md deleted file mode 100644 index 6b18eb497686..000000000000 --- a/.changeset/wet-walls-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes a problem that allowed users to send empty spaces as comment to bypass the "comment required" setting diff --git a/.changeset/wild-spiders-smell.md b/.changeset/wild-spiders-smell.md deleted file mode 100644 index 9694d6259d3a..000000000000 --- a/.changeset/wild-spiders-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where a mailer error was being sent to customers using offline message's form on Omnichannel instead of the translated one diff --git a/.changeset/wise-onions-trade.md b/.changeset/wise-onions-trade.md deleted file mode 100644 index cb5c731fb6fb..000000000000 --- a/.changeset/wise-onions-trade.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch -"@rocket.chat/livechat": patch -"@rocket.chat/mock-providers": patch -"@rocket.chat/ui-client": patch -"@rocket.chat/ui-contexts": patch -"@rocket.chat/web-ui-registration": patch ---- - -Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. diff --git a/.changeset/wise-walls-tan.md b/.changeset/wise-walls-tan.md deleted file mode 100644 index f558de82ec4c..000000000000 --- a/.changeset/wise-walls-tan.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -fix: missing params on updateOwnBasicInfo endpoint diff --git a/.changeset/wise-ways-fetch.md b/.changeset/wise-ways-fetch.md deleted file mode 100644 index a81063813c35..000000000000 --- a/.changeset/wise-ways-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed the unread messages mark not showing diff --git a/.changeset/witty-feet-warn.md b/.changeset/witty-feet-warn.md deleted file mode 100644 index faaa5d44c134..000000000000 --- a/.changeset/witty-feet-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed the video recorder window not closing after permission is denied. diff --git a/.changeset/yellow-buttons-agree.md b/.changeset/yellow-buttons-agree.md deleted file mode 100644 index a86d172a4544..000000000000 --- a/.changeset/yellow-buttons-agree.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/ui-client': minor -'@rocket.chat/meteor': minor ---- - -feat: add ChangePassword field to Account/Security diff --git a/.changeset/yellow-schools-tell.md b/.changeset/yellow-schools-tell.md deleted file mode 100644 index c1040fa0856a..000000000000 --- a/.changeset/yellow-schools-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/eslint-config': minor ---- - -Unpublished changes in ESLint config diff --git a/.changeset/young-trains-glow.md b/.changeset/young-trains-glow.md deleted file mode 100644 index 77f50812143f..000000000000 --- a/.changeset/young-trains-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Fixed the issue of apps icon uneven alignment in case of missing icons inside message composer toolbar & message toolbar menu. diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 18e6e45e8f48..9c2e0b63e240 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,216 @@ # @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 @@ -266,6 +477,31 @@ - @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 diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 4a7aec073442..8e0541b8040b 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -670,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( @@ -680,6 +687,7 @@ async function createChannel( readOnly, params.customFields, params.extraData, + params.excludeSelf, ); return { diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index df54b683fda4..8f2999cee71e 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,4 +1,4 @@ -import { Team } from '@rocket.chat/core-services'; +import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { check, Match } from 'meteor/check'; @@ -26,29 +26,7 @@ import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; -// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -async function findPrivateGroupByIdOrName({ - params, - checkedArchived = true, - userId, -}: { - params: - | { - roomId?: string; - } - | { - roomName?: string; - }; - userId: string; - checkedArchived?: boolean; -}): Promise<{ - rid: string; - open: boolean; - ro: boolean; - t: string; - name: string; - broadcast: boolean; -}> { +async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( (!('roomId' in params) && !('roomName' in params)) || ('roomId' in params && !(params as { roomId?: string }).roomId && 'roomName' in params && !(params as { roomName?: string }).roomName) @@ -68,17 +46,48 @@ async function findPrivateGroupByIdOrName({ broadcast: 1, }, }; - let room: IRoom | null = null; - if ('roomId' in params) { - room = await Rooms.findOneById(params.roomId || '', roomOptions); - } else if ('roomName' in params) { - room = await Rooms.findOneByName(params.roomName || '', roomOptions); - } + + const room = await (() => { + if ('roomId' in params) { + return Rooms.findOneById(params.roomId || '', roomOptions); + } + if ('roomName' in params) { + return Rooms.findOneByName(params.roomName || '', roomOptions); + } + })(); if (!room || room.t !== 'p') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } + return room; +} + +// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property +async function findPrivateGroupByIdOrName({ + params, + checkedArchived = true, + userId, +}: { + params: + | { + roomId?: string; + } + | { + roomName?: string; + }; + userId: string; + checkedArchived?: boolean; +}): Promise<{ + rid: string; + open: boolean; + ro: boolean; + t: string; + name: string; + broadcast: boolean; +}> { + const room = await getRoomFromParams(params); + const user = await Users.findOneById(userId, { projections: { username: 1 } }); if (!room || !user || !(await canAccessRoomAsync(room, user))) { @@ -302,10 +311,6 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!(await hasPermissionAsync(this.userId, 'create-p'))) { - return API.v1.unauthorized(); - } - if (!this.bodyParams.name) { return API.v1.failure('Body param "name" is required'); } @@ -323,24 +328,32 @@ API.v1.addRoute( const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false; - const result = await createPrivateGroupMethod( - this.userId, - this.bodyParams.name, - this.bodyParams.members ? this.bodyParams.members : [], - readOnly, - this.bodyParams.customFields, - this.bodyParams.extraData, - ); - - const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + try { + const result = await createPrivateGroupMethod( + this.user, + this.bodyParams.name, + this.bodyParams.members ? this.bodyParams.members : [], + readOnly, + this.bodyParams.customFields, + this.bodyParams.extraData, + this.bodyParams.excludeSelf ?? false, + ); + + const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + if (!room) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } - if (!room) { - throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + return API.v1.success({ + group: await composeRoomWithLastMessage(room, this.userId), + }); + } catch (error: unknown) { + if (isMeteorError(error) && error.reason === 'error-not-allowed') { + return API.v1.unauthorized(); + } } - return API.v1.success({ - group: await composeRoomWithLastMessage(room, this.userId), - }); + return API.v1.internalError(); }, }, ); @@ -581,17 +594,14 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const findResult = await findPrivateGroupByIdOrName({ - params: this.bodyParams, - userId: this.userId, - }); + const room = await getRoomFromParams(this.bodyParams); const user = await getUserFromParams(this.bodyParams); if (!user?.username) { return API.v1.failure('Invalid user'); } - await removeUserFromRoomMethod(this.userId, { rid: findResult.rid, username: user.username }); + await removeUserFromRoomMethod(this.userId, { rid: room._id, username: user.username }); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index dec4da6bf87b..ae5a79719cce 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -1,13 +1,14 @@ import crypto from 'crypto'; import type { IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import { Settings, Users } from '@rocket.chat/models'; import { isShieldSvgProps, isSpotlightProps, isDirectoryProps, isMethodCallProps, isMethodCallAnonProps, + isFingerprintProps, isMeteorCall, validateParamsPwGetPolicyRest, } from '@rocket.chat/rest-typings'; @@ -16,6 +17,7 @@ import EJSON from 'ejson'; import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; +import { v4 as uuidv4 } from 'uuid'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; @@ -643,3 +645,75 @@ API.v1.addRoute( }, }, ); + +/** + * @openapi + * /api/v1/fingerprint: + * post: + * description: Update Fingerprint definition as a new workspace or update of configuration + * security: + * $ref: '#/security/authenticated' + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * setDeploymentAs: + * type: string + * example: | + * { + * "setDeploymentAs": "new-workspace" + * } + * responses: + * 200: + * description: Workspace successfully configured + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'fingerprint', + { + authRequired: true, + validateParams: isFingerprintProps, + }, + { + async post() { + check(this.bodyParams, { + setDeploymentAs: String, + }); + + if (this.bodyParams.setDeploymentAs === 'new-workspace') { + await Promise.all([ + Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()), + // Settings.resetValueById('Cloud_Url'), + Settings.resetValueById('Cloud_Service_Agree_PrivacyTerms'), + Settings.resetValueById('Cloud_Workspace_Id'), + Settings.resetValueById('Cloud_Workspace_Name'), + Settings.resetValueById('Cloud_Workspace_Client_Id'), + Settings.resetValueById('Cloud_Workspace_Client_Secret'), + Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At'), + Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri'), + Settings.resetValueById('Cloud_Workspace_PublicKey'), + Settings.resetValueById('Cloud_Workspace_License'), + Settings.resetValueById('Cloud_Workspace_Had_Trial'), + Settings.resetValueById('Cloud_Workspace_Access_Token'), + Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)), + Settings.resetValueById('Cloud_Workspace_Registration_State'), + ]); + } + + await Settings.updateValueById('Deployment_FingerPrint_Verified', true); + + return API.v1.success({}); + }, + }, +); diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 71f7387e1aa5..76a0545c8801 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -223,7 +223,7 @@ export class AppLivechatBridge extends LivechatBridge { } return Promise.all( - (await LivechatVisitors.find(query).toArray()).map( + (await LivechatVisitors.findEnabled(query).toArray()).map( async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor), ), ); diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 481292d61790..91b0049513f0 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -55,7 +55,11 @@ export class AppRoomBridge extends RoomBridge { } private async createPrivateGroup(userId: string, room: ICoreRoom, members: string[]): Promise { - return (await createPrivateGroupMethod(userId, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('Invalid user'); + } + return (await createPrivateGroupMethod(user, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; } protected async getById(roomId: string, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index ae38feff5eff..905534212836 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -37,7 +37,7 @@ export class AppRoomsConverter { let v; if (room.visitor) { - const visitor = await LivechatVisitors.findOneById(room.visitor.id); + const visitor = await LivechatVisitors.findOneEnabledById(room.visitor.id); const { lastMessageTs, phone } = room.visitorChannelInfo; diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index ba288c96d7b8..a9f5d450efad 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -9,7 +9,7 @@ export class AppVisitorsConverter { } async convertById(id) { - const visitor = await LivechatVisitors.findOneById(id); + const visitor = await LivechatVisitors.findOneEnabledById(id); return this.convertVisitor(visitor); } diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index fc917028c33f..7b5f1594e5c3 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -10,6 +10,8 @@ export const permissions = [ { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'add-user-to-any-c-room', roles: ['admin'] }, { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'kick-user-from-any-c-room', roles: ['admin'] }, + { _id: 'kick-user-from-any-p-room', roles: [] }, { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, { _id: 'archive-room', roles: ['admin', 'owner'] }, { _id: 'assign-admin-role', roles: ['admin'] }, diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index 2ad8ba29072a..f887c9e6395c 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -7,6 +7,8 @@ import { LICENSE_VERSION } from '../license'; export type WorkspaceRegistrationData = { uniqueId: string; + deploymentFingerprintHash: string; + deploymentFingerprintVerified: boolean; workspaceId: string; address: string; contactName: string; @@ -32,7 +34,10 @@ export type WorkspaceRegistrationData = { setupComplete: boolean; connectionDisable: boolean; npsEnabled: string; + // TODO: Evaluate naming MAC: number; + // activeContactsBillingMonth: number; + // activeContactsYesterday: number; }; export async function buildWorkspaceRegistrationData(contactEmail: T): Promise> { @@ -47,6 +52,8 @@ export async function buildWorkspaceRegistrationData('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 || ''; @@ -56,6 +63,8 @@ export async function buildWorkspaceRegistrationData { 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, @@ -47,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)) { @@ -63,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', }); @@ -140,53 +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 autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(member); + const room = await Rooms.createWithFullRoomData(roomProps); - await Subscriptions.createWithRoomAndUser(room, member, { ...extra, ...autoTranslateConfig }); - } - } + 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) { @@ -195,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/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index ff8182cec8c9..98cea517bed4 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -35,8 +35,7 @@ export const createChannelMethod = async ( throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - + const user = await Users.findOneById(userId); if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } @@ -44,7 +43,7 @@ export const createChannelMethod = async ( if (!(await hasPermissionAsync(userId, 'create-c'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); } - return createRoom('c', name, user.username, members, excludeSelf, readOnly, { + return createRoom('c', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index 65298949a345..75097b5c89b8 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -1,4 +1,4 @@ -import type { ICreatedRoom } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; @@ -21,7 +21,7 @@ declare module '@rocket.chat/ui-contexts' { } export const createPrivateGroupMethod = async ( - userId: string, + user: IUser, name: string, members: string[], readOnly = false, @@ -35,23 +35,12 @@ export const createPrivateGroupMethod = async ( > => { check(name, String); check(members, Match.Optional([String])); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - if (!(await hasPermissionAsync(userId, 'create-p'))) { + if (!(await hasPermissionAsync(user._id, 'create-p'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } - return createRoom('p', name, user.username, members, excludeSelf, readOnly, { + return createRoom('p', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); @@ -67,6 +56,13 @@ Meteor.methods({ }); } - return createPrivateGroupMethod(uid, name, members, readOnly, customFields, extraData); + const user = await Users.findOneById(uid); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createPrivateGroup', + }); + } + + return createPrivateGroupMethod(user, name, members, readOnly, customFields, extraData); }, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js index 6521c0f662dd..7ecb3b3fc100 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.js +++ b/apps/meteor/app/livechat/imports/server/rest/sms.js @@ -56,7 +56,7 @@ const defineVisitor = async (smsNumber, targetDepartment) => { } const id = await LivechatTyped.registerGuest(data); - return LivechatVisitors.findOneById(id); + return LivechatVisitors.findOneEnabledById(id); }; const normalizeLocationSharing = (payload) => { diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.ts b/apps/meteor/app/livechat/server/api/lib/visitors.ts index e559aecc892e..0abed5197d78 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.ts +++ b/apps/meteor/app/livechat/server/api/lib/visitors.ts @@ -6,7 +6,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { - const visitor = await LivechatVisitors.findOneById(visitorId); + const visitor = await LivechatVisitors.findOneEnabledById(visitorId); if (!visitor) { throw new Error('visitor-not-found'); } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 517acf33f137..57c1d117f1b0 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -33,7 +33,7 @@ API.v1.addRoute( contactId: String, }); - const contact = await LivechatVisitors.findOneById(this.queryParams.contactId); + const contact = await LivechatVisitors.findOneEnabledById(this.queryParams.contactId); return API.v1.success({ contact }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 2b6f4c00af53..104e2ece94d5 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -269,7 +269,7 @@ API.v1.addRoute( guest.connectionData = normalizeHttpHeaderData(this.request.headers); const visitorId = await LivechatTyped.registerGuest(guest); - visitor = await LivechatVisitors.findOneById(visitorId); + visitor = await LivechatVisitors.findOneEnabledById(visitorId); } const sentMessages = await Promise.all( diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 0fe60248bfba..86629e636bf8 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -326,7 +326,7 @@ API.v1.addRoute( throw new Error('This_conversation_is_already_closed'); } - const guest = await LivechatVisitors.findOneById(room.v?._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); const transferedBy = this.user satisfies TransferByData; transferData.transferredBy = normalizeTransferredByData(transferedBy, room); if (transferData.userId) { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 012b412639ea..ae9d1ea4fd83 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -45,7 +45,7 @@ API.v1.addRoute('livechat/visitor', { const visitorId = await LivechatTyped.registerGuest(guest); - let visitor = await VisitorsRaw.findOneById(visitorId, {}); + let visitor = await VisitorsRaw.findOneEnabledById(visitorId, {}); if (visitor) { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); // If it's updating an existing visitor, it must also update the roomInfo @@ -65,7 +65,7 @@ API.v1.addRoute('livechat/visitor', { } } - visitor = await VisitorsRaw.findOneById(visitorId, {}); + visitor = await VisitorsRaw.findOneEnabledById(visitorId, {}); } if (!visitor) { @@ -122,7 +122,7 @@ API.v1.addRoute('livechat/visitor/:token', { const { _id } = visitor; const result = await Livechat.removeGuest(_id); - if (!result) { + if (!result.modifiedCount) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 5ebf924e7334..ad68fcf5ce5c 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -1,6 +1,7 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; -import { LivechatRooms } from '@rocket.chat/models'; +import { LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; +import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; @@ -26,6 +27,19 @@ callbacks.add( return message; } + // Return YYYY-MM from moment + const monthYear = moment().format('YYYY-MM'); + const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear); + if (!isVisitorActive) { + await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear); + } + + await LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear); + + if (room.responseBy) { + await LivechatRooms.setAgentLastMessageTs(room._id); + } + // check if room is yet awaiting for response from visitor if (!room.waitingResponse) { // case where agent sends second message or any subsequent message in a room before visitor responds to the first message diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 52138740e295..c560f3dd7aa7 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -285,7 +285,7 @@ export const Livechat = { Livechat.logger.debug(`Closing open chats for user ${userId}`); const user = await Users.findOneById(userId); - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}); + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); const promises = []; await openChats.forEach((room) => { @@ -298,7 +298,7 @@ export const Livechat = { async forwardOpenChats(userId) { Livechat.logger.debug(`Transferring open chats for user ${userId}`); for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneById(room.v._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); const user = await Users.findOneById(userId); const { _id, username, name } = user; const transferredBy = normalizeTransferredByData({ _id, username, name }, room); @@ -462,7 +462,7 @@ export const Livechat = { }, async getLivechatRoomGuestInfo(room) { - const visitor = await LivechatVisitors.findOneById(room.v._id); + const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); const agent = await Users.findOneById(room.servedBy && room.servedBy._id); const ua = new UAParser(); @@ -604,16 +604,15 @@ export const Livechat = { }, async removeGuest(_id) { - check(_id, String); - const guest = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); + const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest', }); } - await this.cleanGuestHistory(_id); - return LivechatVisitors.removeById(_id); + await this.cleanGuestHistory(guest); + return LivechatVisitors.disableById(_id); }, async setUserStatusLivechat(userId, status) { @@ -628,16 +627,13 @@ export const Livechat = { return user; }, - async cleanGuestHistory(_id) { - const guest = await LivechatVisitors.findOneById(_id); - if (!guest) { - throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { - method: 'livechat:cleanGuestHistory', - }); - } - + async cleanGuestHistory(guest) { const { token } = guest; - check(token, String); + + // This shouldn't be possible, but just in case + if (!token) { + throw new Error('error-invalid-guest'); + } const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const cursor = LivechatRooms.findByVisitorToken(token, extraQuery); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 1c60a257d319..c443bc7873c7 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -305,7 +305,7 @@ class LivechatClass { !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) ) { await LivechatVisitors.removeDepartmentById(guest._id); - const tmpGuest = await LivechatVisitors.findOneById(guest._id); + const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id); if (tmpGuest) { guest = tmpGuest; } diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index 2dc796fc6c94..3817b10bf42b 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -58,7 +58,7 @@ Meteor.methods({ }); } - const guest = await LivechatVisitors.findOneById(room.v?._id); + const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); const user = await Meteor.userAsync(); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 06c3014a8a56..f62ab71f2302 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -480,7 +480,6 @@ export class SAML { continue; } - const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); const privRoom = await Rooms.findOneByNameAndType(roomName, 'p', {}); if (privRoom && includePrivateChannelsInUpdate === true) { @@ -488,6 +487,7 @@ export class SAML { continue; } + const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); if (room) { await addUserToRoom(room._id, user); continue; @@ -496,7 +496,7 @@ export class SAML { if (!room && !privRoom) { // If the user doesn't have an username yet, we can't create new rooms for them if (user.username) { - await createRoom('c', roomName, user.username); + await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index a3c70f012fa1..104d50c56926 100644 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ b/apps/meteor/app/slashcommands-create/server/server.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; @@ -50,7 +50,11 @@ slashCommands.add({ } if (getParams(params).indexOf('private') > -1) { - await createPrivateGroupMethod(userId, channelStr, []); + const user = await Users.findOneById(userId); + if (!user) { + return; + } + await createPrivateGroupMethod(user, channelStr, []); return; } diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 9917775aca06..5376bd6ae64b 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -37,6 +37,9 @@ function inviteAll(type: T): SlashCommand['callback'] { } const user = await Users.findOneById(userId); + if (!user) { + return; + } const lng = user?.language || settings.get('Language') || 'en'; const baseChannel = type === 'to' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel); @@ -69,7 +72,7 @@ function inviteAll(type: T): SlashCommand['callback'] { const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { - baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(userId, channel, users); + baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(user, channel, users); void api.broadcast('notify.ephemeralMessage', userId, message.rid, { msg: i18n.t('Channel_created', { postProcess: 'sprintf', diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 89b068c11341..64543deb88a1 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -24,8 +24,10 @@ 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/getStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; @@ -99,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; @@ -269,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) { diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 5618442ee6da..5807673188e5 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -29,7 +29,7 @@ Meteor.startup(async () => { id: 'reply-directly', icon: 'reply-directly', label: 'Reply_in_direct_message', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], role: 'link', type: 'communication', action(_, props) { @@ -122,7 +122,7 @@ Meteor.startup(async () => { icon: 'permalink', label: 'Copy_link', // classes: 'clipboard', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], type: 'duplication', async action(_, props) { try { @@ -208,7 +208,7 @@ Meteor.startup(async () => { id: 'delete-message', icon: 'trash', label: 'Delete', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', async action(this: unknown, _, { message = messageArgs(this).msg, chat }) { @@ -236,7 +236,7 @@ Meteor.startup(async () => { id: 'report-message', icon: 'report', label: 'Report', - context: ['message', 'message-mobile', 'threads', 'federated'], + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', action(this: unknown, _, { message = messageArgs(this).msg }) { @@ -264,7 +264,7 @@ Meteor.startup(async () => { id: 'reaction-list', icon: 'emoji', label: 'Reactions', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'interaction', action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { imperativeModal.open({ diff --git a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts index a388548c18a8..adb4c2ab1ae9 100644 --- a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts +++ b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts @@ -1,4 +1,4 @@ -import type { ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings'; /** * @type {(userPref: Pick) => { @@ -7,7 +7,7 @@ import type { ISubscription, IUser } from '@rocket.chat/core-typings'; * emailPrefOrigin: 'user'; * }} */ -export const getDefaultSubscriptionPref = (userPref: IUser) => { +export const getDefaultSubscriptionPref = (userPref: AtLeast) => { const subscription: Partial = {}; const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {}; diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index 50d53da351bc..38aadf0a840b 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -17,7 +17,7 @@ type AutoCompleteDepartmentMultipleProps = { }; const AutoCompleteDepartmentMultiple = ({ - value, + value = [], onlyMyDepartments = false, showArchived = false, enabled = false, @@ -37,6 +37,11 @@ const AutoCompleteDepartmentMultiple = ({ const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); + const departmentOptions = useMemo(() => { + const pending = value.filter(({ value }) => !departmentsItems.find((dep) => dep.value === value)) || []; + return [...departmentsItems, ...pending]; + }, [departmentsItems, value]); + return ( ) => } + wrapperFunction={(props) => } > {t('Discussion_title')} - + + {t('Discussion_description')} - {t('Discussion_description')} - - - {t('Discussion_target_channel')} + + {t('Discussion_target_channel')} + {defaultParentRoom && ( } /> )} - {!defaultParentRoom && ( ( + rules={{ required: t('error-the-field-is-required', { field: t('Discussion_target_channel') }) }} + render={({ field: { name, onBlur, onChange, value } }) => ( )} /> )} - {errors.parentRoom && {errors.parentRoom.message}} + {errors.parentRoom && ( + + {errors.parentRoom.message} + + )} - - - {t('Encrypted')} - - ( - + + {t('Encrypted')} + + } /> - )} - /> + + - {t('Discussion_name')} + + {t('Discussion_name')} + - } + ( + } + /> + )} /> - {errors.name && {errors.name.message}} + {errors.name && ( + + {errors.name.message} + + )} - {t('Invite_Users')} - + {t('Invite_Users')} + ( - + render={({ field: { name, onChange, value, onBlur } }) => ( + )} /> - {t('Discussion_first_message_title')} + {t('Discussion_first_message_title')} - + ( + + )} + /> - {encrypted && {t('Discussion_first_message_disabled_due_to_e2e')}} + {encrypted && {t('Discussion_first_message_disabled_due_to_e2e')}} - diff --git a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx index 0a2717a65552..6036f14049a4 100644 --- a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx +++ b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx @@ -1,38 +1,42 @@ import { Skeleton, TextInput, Callout } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import { AsyncStatePhase } from '../../hooks/useAsyncState'; -import { useEndpointData } from '../../hooks/useEndpointData'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; const DefaultParentRoomField = ({ defaultParentRoom }: { defaultParentRoom: string }): ReactElement => { const t = useTranslation(); - const { value, phase } = useEndpointData('/v1/rooms.info', { - params: useMemo( - () => ({ - roomId: defaultParentRoom, - }), - [defaultParentRoom], - ), + + const query = useMemo( + () => ({ + roomId: defaultParentRoom, + }), + [defaultParentRoom], + ); + + const roomsInfoEndpoint = useEndpoint('GET', '/v1/rooms.info'); + + const { data, isLoading, isError } = useQuery(['defaultParentRoomInfo', query], async () => roomsInfoEndpoint(query), { + refetchOnWindowFocus: false, }); - if (phase === AsyncStatePhase.LOADING) { + if (isLoading) { return ; } - if (!value || !value.room) { + if (!data?.room || isError) { return {t('Error')}; } return ( diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx new file mode 100644 index 000000000000..db4c33654a92 --- /dev/null +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -0,0 +1,44 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import GenericModal from './GenericModal'; + +type FingerprintChangeModalProps = { + onConfirm: () => void; + onCancel: () => void; + onClose: () => void; +}; + +const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { + const t = useTranslation(); + return ( + + + + + ); +}; + +export default FingerprintChangeModal; diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx new file mode 100644 index 000000000000..77718de0f441 --- /dev/null +++ b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx @@ -0,0 +1,47 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import GenericModal from './GenericModal'; + +type FingerprintChangeModalConfirmationProps = { + onConfirm: () => void; + onCancel: () => void; + newWorkspace: boolean; +}; + +const FingerprintChangeModalConfirmation = ({ + onConfirm, + onCancel, + newWorkspace, +}: FingerprintChangeModalConfirmationProps): ReactElement => { + const t = useTranslation(); + return ( + + + + + ); +}; + +export default FingerprintChangeModalConfirmation; diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx index b78df7688741..bdbde6b05acd 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx @@ -77,7 +77,7 @@ const ForwardChatModal = ({ const endReached = useCallback( (start) => { - if (departmentsPhase === AsyncStatePhase.LOADING) { + if (departmentsPhase !== AsyncStatePhase.LOADING) { loadMoreDepartments(start, Math.min(50, departmentsTotal)); } }, diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index 4674528a483f..5552e6da0745 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -46,7 +46,7 @@ const MessageContentBody = ({ mentions, channels, md, searchText }: MessageConte text-decoration: underline; } &:focus { - border: 2px solid ${Palette.stroke['stroke-extra-light-highlight']}; + box-shadow: 0 0 0 2px ${Palette.stroke['stroke-extra-light-highlight']}; border-radius: 2px; } } diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 03be723e0b7f..2cf47066c4e4 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -78,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; } } } diff --git a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx index df45740d9271..dd1ce944f423 100644 --- a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx @@ -1,60 +1,88 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box, Modal, Button } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, FC } from 'react'; -import React, { useState, memo } from 'react'; +import { Box, Modal, Button, FieldGroup, Field, FieldRow, FieldLabel, FieldError } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React, { memo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useEndpointAction } from '../../hooks/useEndpointAction'; import { goToRoomById } from '../../lib/utils/goToRoomById'; -type Username = Exclude; - -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/UserMenu.tsx b/apps/meteor/client/sidebar/header/UserMenu.tsx index 9fcc7a0d2274..a53836eda311 100644 --- a/apps/meteor/client/sidebar/header/UserMenu.tsx +++ b/apps/meteor/client/sidebar/header/UserMenu.tsx @@ -24,6 +24,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} @@ -36,6 +37,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } medium + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} diff --git a/apps/meteor/client/startup/rootUrlChange.ts b/apps/meteor/client/startup/rootUrlChange.ts index 45f98634a373..4e42874eba4a 100644 --- a/apps/meteor/client/startup/rootUrlChange.ts +++ b/apps/meteor/client/startup/rootUrlChange.ts @@ -6,6 +6,8 @@ import { Roles } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; +import FingerprintChangeModal from '../components/FingerprintChangeModal'; +import FingerprintChangeModalConfirmation from '../components/FingerprintChangeModalConfirmation'; import UrlChangeModal from '../components/UrlChangeModal'; import { imperativeModal } from '../lib/imperativeModal'; import { dispatchToastMessage } from '../lib/toast'; @@ -58,3 +60,72 @@ Meteor.startup(() => { return c.stop(); }); }); + +Meteor.startup(() => { + Tracker.autorun((c) => { + const userId = Meteor.userId(); + if (!userId) { + return; + } + + if (!Roles.ready.get() || !isSyncReady.get()) { + return; + } + + if (hasRole(userId, 'admin') === false) { + return c.stop(); + } + + const deploymentFingerPrintVerified = settings.get('Deployment_FingerPrint_Verified'); + if (deploymentFingerPrintVerified == null || deploymentFingerPrintVerified === true) { + return; + } + + const updateWorkspace = (): void => { + imperativeModal.close(); + void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'updated-configuration' }).then(() => { + dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') }); + }); + }; + + const setNewWorkspace = (): void => { + imperativeModal.close(); + void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'new-workspace' }).then(() => { + dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') }); + }); + }; + + const openModal = (): void => { + imperativeModal.open({ + component: FingerprintChangeModal, + props: { + onConfirm: () => { + imperativeModal.open({ + component: FingerprintChangeModalConfirmation, + props: { + onConfirm: setNewWorkspace, + onCancel: openModal, + newWorkspace: true, + }, + }); + }, + onCancel: () => { + imperativeModal.open({ + component: FingerprintChangeModalConfirmation, + props: { + onConfirm: updateWorkspace, + onCancel: openModal, + newWorkspace: false, + }, + }); + }, + onClose: imperativeModal.close, + }, + }); + }; + + openModal(); + + return c.stop(); + }); +}); diff --git a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx index 8a998af348c0..b92ca74d0f6e 100644 --- a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx +++ b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx @@ -13,22 +13,21 @@ const MentionsWithSymbolUpsellModal = ({ onClose }: { onClose: () => void }) => if (!isAdmin) { return ( ); } return ( )} - {t('Name')} + {t('Name')} diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index dede0b34b918..fcdaa29c9dff 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 = [ @@ -46,14 +46,25 @@ const roomTypeFilterStructure = [ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch> }): ReactElement => { const t = useTranslation(); const [text, setText] = useState(''); - const [roomTypeOptions, setRoomTypeOptions] = useState(roomTypeFilterStructure); + const [roomTypeSelectedOptions, setRoomTypeSelectedOptions] = useState([]); - useEffect(() => { - return setFilters({ searchText: text, types: roomTypeSelectedOptions }); - }, [setFilters, roomTypeSelectedOptions, 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/omnichannel/agents/AgentsTable/AgentsTable.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx index a636433eaacb..215155e97cca 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/customFields/CustomFieldsTable.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx index ce7780181ddc..f9872c710a5e 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx @@ -43,7 +43,9 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) ); const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields'); - const { data, isSuccess, isLoading, refetch } = useQuery(['livechat-customFields', query], async () => getCustomFields(query)); + const { data, isSuccess, isLoading, refetch } = useQuery(['livechat-customFields', query, debouncedFilter], async () => + getCustomFields(query), + ); const [defaultQuery] = useState(hashQueryKey([query])); const queryHasChanged = defaultQuery !== hashQueryKey([query]); @@ -105,7 +107,7 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) {isSuccess && data.customFields.length > 0 && ( <> - + {headers} {data.customFields.map(({ label, _id, scope, visibility }) => ( diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx index 46919307b052..bb1d5b057437 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx @@ -15,10 +15,10 @@ import { Button, PaginatedSelectFiltered, } from '@rocket.chat/fuselage'; -import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -130,10 +130,13 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen } = useForm({ mode: 'onChange', defaultValues: initialValues }); const requestTagBeforeClosingChat = watch('requestTagBeforeClosingChat'); - const offlineMessageChannelName = watch('offlineMessageChannelName'); + + const [fallbackFilter, setFallbackFilter] = useState(''); + + const debouncedFallbackFilter = useDebouncedValue(fallbackFilter, 500); const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( - useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), + useMemo(() => ({ text: debouncedFallbackFilter }), [debouncedFallbackFilter]), ); const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); @@ -324,13 +327,14 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen value={value} onChange={onChange} flexShrink={0} - filter={value} - setFilter={onChange} + filter={fallbackFilter} + setFilter={setFallbackFilter as (value?: string | number) => void} options={roomsItems} placeholder={t('Channel_name')} endReached={ roomsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => loadMoreRooms(start, Math.min(50, roomsTotal)) } + aria-busy={fallbackFilter !== debouncedFallbackFilter} /> )} /> diff --git a/apps/meteor/client/views/omnichannel/sidebarItems.ts b/apps/meteor/client/views/omnichannel/sidebarItems.ts index 048bcb4ef88e..7942764a8b89 100644 --- a/apps/meteor/client/views/omnichannel/sidebarItems.ts +++ b/apps/meteor/client/views/omnichannel/sidebarItems.ts @@ -13,12 +13,6 @@ export const { i18nLabel: 'Current_Chats', permissionGranted: (): boolean => hasPermission('view-livechat-current-chats'), }, - { - href: '/omnichannel/reports', - icon: 'file', - i18nLabel: 'Reports', - permissionGranted: (): boolean => hasPermission('view-livechat-reports'), - }, { href: '/omnichannel/analytics', icon: 'dashboard', diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7f376341992d..54ce71bd80ec 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -300,8 +300,9 @@ export const useQuickActions = (): { const manualOnHoldAllowed = useSetting('Livechat_allow_manual_on_hold'); const hasManagerRole = useRole('livechat-manager'); + const hasMonitorRole = useRole('livechat-monitor'); - const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole) && room?.lastMessage?.t !== 'livechat-close'; + const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole || hasMonitorRole) && room?.lastMessage?.t !== 'livechat-close'; const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined; const canForwardGuest = usePermission('transfer-livechat-guest'); const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js index be7a8b1e4238..22c84fbdebf1 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js @@ -9,6 +9,9 @@ import { Callout, NumberInput, FieldGroup, + FieldLabel, + FieldRow, + FieldHint, Button, ButtonGroup, Box, @@ -309,87 +312,87 @@ function EditChannel({ room, onClickClose, onClickBack }) { - {t('Name')} - + {t('Name')} + - + {canViewDescription && ( - {t('Description')} - + {t('Description')} + - + )} {canViewAnnouncement && ( - {t('Announcement')} - + {t('Announcement')} + - + )} {canViewTopic && ( - {t('Topic')} - + {t('Topic')} + - + )} {canViewType && ( - {t('Private')} - + {t('Private')} + - + - {t('Teams_New_Private_Description_Enabled')} + {t('Teams_New_Private_Description_Enabled')} )} {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('Only_authorized_users_can_react_to_messages')} + {t('Only_authorized_users_can_react_to_messages')} )} {canViewArchived && ( - {t('Room_archivation_state_true')} - + {t('Room_archivation_state_true')} + - + )} {canViewJoinCode && ( - {t('Password_to_access')} - + {t('Password_to_access')} + - + - + - + )} {canViewHideSysMes && ( - {t('Hide_System_Messages')} - + {t('Hide_System_Messages')} + - + - + - + )} {canViewEncrypted && ( - {t('Encrypted')} - + {t('Encrypted')} + - + )} @@ -437,22 +440,22 @@ function EditChannel({ room, onClickClose, onClickBack }) { - {t('RetentionPolicyRoom_Enabled')} - + {t('RetentionPolicyRoom_Enabled')} + - + - {t('RetentionPolicyRoom_OverrideGlobal')} - + {t('RetentionPolicyRoom_OverrideGlobal')} + - + {retentionOverrideGlobal && ( @@ -461,25 +464,25 @@ function EditChannel({ room, onClickClose, onClickBack }) { {t('RetentionPolicyRoom_ReadTheDocs')} - {t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })} - + {t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })} + - + - {t('RetentionPolicyRoom_ExcludePinned')} - + {t('RetentionPolicyRoom_ExcludePinned')} + - + - {t('RetentionPolicyRoom_FilesOnly')} - + {t('RetentionPolicyRoom_FilesOnly')} + - + diff --git a/apps/meteor/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts b/apps/meteor/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts index 4d13df42c104..1cb016a33be6 100644 --- a/apps/meteor/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts +++ b/apps/meteor/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts @@ -48,7 +48,7 @@ const handleBeforeSaveMessage = async (message: IMessage, room?: IOmnichannelRoo } const visitorId = room?.v?._id; const agent = (await Users.findOneById(agentId, { projection: { name: 1, _id: 1, emails: 1 } })) || {}; - const visitor = visitorId && ((await LivechatVisitors.findOneById(visitorId, {})) || {}); + const visitor = visitorId && ((await LivechatVisitors.findOneEnabledById(visitorId, {})) || {}); Object.keys(placeholderFields).map((field) => { const templateKey = `{{${field}}}`; diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts b/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts index 14f9f4b110c8..c89931208451 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts +++ b/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts @@ -1,6 +1,13 @@ import { hasPermission, hasAtLeastOnePermission } from '../../../../../app/authorization/client'; import { registerOmnichannelSidebarItem } from '../../../../../client/views/omnichannel/sidebarItems'; +registerOmnichannelSidebarItem({ + href: '/omnichannel/reports', + icon: 'file', + i18nLabel: 'Reports', + permissionGranted: (): boolean => hasPermission('view-livechat-reports'), +}); + registerOmnichannelSidebarItem({ href: '/omnichannel/monitors', icon: 'shield-blank', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts index 3c96cad39b72..d609d8464b04 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts @@ -4,16 +4,17 @@ import type { FilterOperators } from 'mongodb'; import { hasRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; -import { getUnitsFromUser } from '../lib/units'; +import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles'; -export const addQueryRestrictionsToDepartmentsModel = async (originalQuery: FilterOperators = {}) => { +export const addQueryRestrictionsToDepartmentsModel = async (originalQuery: FilterOperators = {}, userId: string) => { const query: FilterOperators = { ...originalQuery, type: { $ne: 'u' } }; - const units = await getUnitsFromUser(); + const units = await getUnitsFromUser(userId); if (Array.isArray(units)) { query.ancestors = { $in: units }; } + cbLogger.debug({ msg: 'Applying department query restrictions', userId, units }); return query; }; @@ -25,7 +26,7 @@ callbacks.add( } cbLogger.debug('Applying department query restrictions'); - return addQueryRestrictionsToDepartmentsModel(originalQuery); + return addQueryRestrictionsToDepartmentsModel(originalQuery, userId); }, callbacks.priority.HIGH, 'livechat-apply-department-restrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts index 1a18b92dc94d..597a7546e99a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -3,6 +3,7 @@ import { LivechatDepartment } from '@rocket.chat/models'; import type { FilterOperators } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; +import { cbLogger } from '../lib/logger'; import { getUnitsFromUser } from '../lib/units'; export const restrictQuery = async (originalQuery: FilterOperators = {}) => { @@ -20,6 +21,7 @@ export const restrictQuery = async (originalQuery: FilterOperators>(visitorId, { + const visitor = await LivechatVisitors.findOneEnabledById>(visitorId, { projection: { name: 1, username: 1 }, }); if (!visitor) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index 824296d1e673..12233a9127a8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -148,7 +148,7 @@ export class VisitorInactivityMonitor { } private async getDefaultAbandonedCustomMessage(abandonmentAction: 'close' | 'on-hold', visitorId: string) { - const visitor = await LivechatVisitors.findOneById>(visitorId, { + const visitor = await LivechatVisitors.findOneEnabledById>(visitorId, { projection: { name: 1, username: 1, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts index dd0f3867f574..47929f384d8f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts @@ -1,11 +1,23 @@ -import { LivechatUnit } from '@rocket.chat/models'; +import { LivechatUnit, LivechatDepartmentAgents } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import mem from 'mem'; import { Meteor } from 'meteor/meteor'; import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; +import { logger } from '../lib/logger'; -async function getUnitsFromUserRoles(user: string | null): Promise { +async function getUnitsFromUserRoles(user: string): Promise { + return LivechatUnit.findByMonitorId(user); +} + +async function getDepartmentsFromUserRoles(user: string): Promise { + return (await LivechatDepartmentAgents.findByAgentId(user).toArray()).map((department) => department.departmentId); +} + +const memoizedGetUnitFromUserRoles = mem(getUnitsFromUserRoles, { maxAge: 10000 }); +const memoizedGetDepartmentsFromUserRoles = mem(getDepartmentsFromUserRoles, { maxAge: 5000 }); + +export const getUnitsFromUser = async (user: string): Promise => { if (!user || (await hasAnyRoleAsync(user, ['admin', 'livechat-manager']))) { return; } @@ -14,10 +26,11 @@ async function getUnitsFromUserRoles(user: string | null): Promise({ - 'livechat:getUnitsFromUser'(): Promise { + async 'livechat:getUnitsFromUser'(): Promise { const user = Meteor.userId(); - return memoizedGetUnitFromUserRoles(user); + if (!user) { + return; + } + return getUnitsFromUser(user); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 2507dd683145..99a80b3b0b88 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -19,7 +19,7 @@ async function resolveOnHoldCommentInfo(options: { clientAction: boolean }, room const { v: { _id: visitorId }, } = room; - const visitor = await LivechatVisitors.findOneById>(visitorId, { + const visitor = await LivechatVisitors.findOneEnabledById>(visitorId, { projection: { name: 1, username: 1 }, }); if (!visitor) { diff --git a/apps/meteor/ee/client/hooks/useTagsList.ts b/apps/meteor/ee/client/hooks/useTagsList.ts index 467d92103ea2..907fa96f4aab 100644 --- a/apps/meteor/ee/client/hooks/useTagsList.ts +++ b/apps/meteor/ee/client/hooks/useTagsList.ts @@ -38,6 +38,7 @@ export const useTagsList = (options: TagsListOptions): UseTagsListResult => { count: end + start, ...(viewAll && { viewAll: 'true' }), ...(department && { department }), + sort: JSON.stringify({ name: 1 }), }); return { @@ -45,7 +46,6 @@ export const useTagsList = (options: TagsListOptions): UseTagsListResult => { _id: tag._id, label: tag.name, value: tag.name, - _updatedAt: new Date(tag._updatedAt), })), itemCount: total, }; diff --git a/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx b/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx index 3bbe82494bcc..147dff65ad97 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/tags/AutoCompleteTagsMultiple.js b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js index 086d796152fc..db48060ff546 100644 --- a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js +++ b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js @@ -24,16 +24,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} diff --git a/apps/meteor/ee/client/startup/readReceipt.ts b/apps/meteor/ee/client/startup/readReceipt.ts index 0efc779a3b49..938cc4b6133f 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/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index deb6cdcec666..6c04574ad557 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 b24d7436a784..b75c8aa9a7a5 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/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index b39e3d9eacfa..3295af1b6179 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -11,7 +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 { addQueryRestrictionsToRoomsModel } from '../../../app/livechat-enterprise/server/lib/query.helper'; declare module '@rocket.chat/model-typings' { interface ILivechatRoomsModel { @@ -296,32 +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); - 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); - return super.updateOne(restrictedQuery, update, opts); - } - - async updateMany(...args: Parameters) { - const [query, ...restArgs] = args; - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - return super.updateMany(restrictedQuery, ...restArgs); - } - getConversationsBySource(start: Date, end: Date, extraQuery: Filter): AggregationCursor { return this.col.aggregate( [ diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index 180b145e4352..fcabf12fa4f8 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -51,15 +51,6 @@ export class LivechatUnitRaw extends BaseRaw implement return this.col.findOne(query, options); } - async update( - originalQuery: Filter, - update: Filter, - options: FindOptions, - ): Promise { - const query = await addQueryRestrictions(originalQuery); - return this.col.updateOne(query, update, options); - } - remove(query: Filter): Promise { return this.deleteMany(query); } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index cb58d45affa3..77b48a0d1f92 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,45 @@ # 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 diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 98658d1ab681..ea6660730c63 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.8-rc.4", + "version": "1.1.9", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index 5731ca0d1deb..bfcb1ba5fa8b 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -8,7 +8,7 @@ callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (await License.shouldPreventAction('roomsPerGuest', { userId: user._id })) { + 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 f6d42823cb97..e72852052acc 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (await License.shouldPreventAction('activeUsers', {}, userCount)) { + if (await License.shouldPreventAction('activeUsers', userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 9c7333a355b3..169144cc2788 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -17,6 +17,7 @@ import type { InquiryWithAgentInfo, ILivechatTagRecord, TransferData, + AtLeast, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -56,7 +57,7 @@ interface EventLikeCallbackSignatures { '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; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 94ce81c32284..91993ff443fb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -887,6 +887,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", @@ -1092,6 +1093,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._", @@ -1105,8 +1107,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", @@ -1673,7 +1681,7 @@ "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", @@ -1848,6 +1856,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", @@ -2753,6 +2762,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", @@ -3339,10 +3352,7 @@ "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_title": "Enable Mentions with symbol", - "Mentions_with_symbol_upsell_subtitle": "Enhance your team's experience with @ symbol on mentions", - "Mentions_with_symbol_upsell_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_annotation": "Talk to your workspace admin about enabling mentions with symbol for everyone.", + "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", @@ -3653,6 +3663,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", @@ -5254,6 +5266,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", 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 000000000000..a71b3b837d9a 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 000000000000..b328d79a023a --- /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/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index d71190cb0b6d..939d91661650 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -35,7 +35,7 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr return guest; } await LivechatTyped.setDepartmentForGuest({ token: guest.token, department }); - return LivechatVisitors.findOneById(guest._id, {}); + return LivechatVisitors.findOneEnabledById(guest._id, {}); } return guest; } @@ -47,7 +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; } diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts index 13540246f0e6..92e76d8c2ec1 100644 --- a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts +++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts @@ -1,28 +1,23 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Settings } from '@rocket.chat/models'; +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; -export const getSubscriptionAutotranslateDefaultConfig = async ( - user: IUser, -): Promise< +import { settings } from '../../app/settings/server'; + +export function getSubscriptionAutotranslateDefaultConfig(user: AtLeast): | { autoTranslate: boolean; autoTranslateLanguage: string; } - | undefined -> => { - const [autoEnableSetting, languageSetting] = await Promise.all([ - Settings.findOneById('AutoTranslate_AutoEnableOnJoinRoom'), - Settings.findOneById('Language'), - ]); - const { language: userLanguage } = user.settings?.preferences || {}; - - if (!autoEnableSetting?.value) { + | undefined { + if (!settings.get('AutoTranslate_AutoEnableOnJoinRoom')) { return; } - if (!userLanguage || userLanguage === 'default' || languageSetting?.value === userLanguage) { + 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 da3aeec761e6..f70b5bcca9ff 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 395056903ae4..a064553f5cb4 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/livechat.ts b/apps/meteor/server/lib/rooms/roomTypes/livechat.ts index 88393088541e..92d722ac2bb0 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/main.ts b/apps/meteor/server/main.ts index 09edca701540..b9418fe43830 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'; diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index acba1bed406b..11232908b847 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -56,7 +56,7 @@ Meteor.methods({ continue; } await callbacks.run('beforeJoinRoom', user, room); - const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts index d92c7e46292e..ccbfe8916cae 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/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index ea5bfa9edcff..2f29b1f55039 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/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 38eab9056586..974c2b5cb570 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, - }, ); } @@ -2448,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 2df2ae09882b..7b478bab43d6 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/Settings.ts b/apps/meteor/server/models/raw/Settings.ts index 3a5d150c0158..1154b7dfe630 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 4b42367bad05..c4ba44bdd7f9 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'; @@ -1605,6 +1615,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 0663bbdcda28..113f18ea83da 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -384,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, { @@ -1488,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( { 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 018a5f87704c..c4aee8bcf2aa 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/room/service.ts b/apps/meteor/server/services/room/service.ts index 61b5bfeee504..7b9b85cecbd0 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -23,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 }> { diff --git a/apps/meteor/server/settings/misc.ts b/apps/meteor/server/settings/misc.ts index 127d0e6e97ba..fa7b6bbde3d0 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 fe5d27c1e677..cc9da5474862 100644 --- a/apps/meteor/server/settings/omnichannel.ts +++ b/apps/meteor/server/settings/omnichannel.ts @@ -778,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 62da3f1471cf..9799c2017afd 100644 --- a/apps/meteor/server/settings/setup-wizard.ts +++ b/apps/meteor/server/settings/setup-wizard.ts @@ -1270,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/startup/migrations/xrun.js b/apps/meteor/server/startup/migrations/xrun.js index bd3d19a7cbee..1af7cb8ad8ad 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 e11324a47a46..8aba28addfcf 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 c2658c73af8d..5efb279dcb18 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 7a5dc23b4cc0..38fb176faaa4 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 89b6af709fbf..b6fd3a4bf6b3 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/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index 61a2719d9cba..c12b875783ab 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 a3ca544de20c..55ef4402da39 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/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 2b8f68d82542..81958cf94cc6 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,45 @@ # @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 diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 5828b25ebcd3..9038b8cf2ef0 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index c88f11bdaca0..e1f254d06053 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,45 @@ # @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 diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 9eac1c8b04fc..8aff178aeecd 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 1054c42ff2de..1bafaf49ea7e 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,51 @@ # @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 diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 893bc8f09509..aed89a2d1c45 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.8-rc.4", + "version": "0.1.9", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index eea1e8489b1b..cb28d7d7a3ee 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,45 @@ # @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 diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 851626cbe980..038055b4fb73 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 7c2478a73718..f000298cbd18 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,42 @@ # @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 diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 9dd31e8c0ffd..87c76b4ff5c8 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index adb2e6c38e2e..a0abfb00c5c8 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,41 @@ # @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 diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 31aacc6c7c1b..156af4fdc0e6 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index eb569a157edc..d76068538d9c 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,39 @@ # @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 diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 43753c77c78d..cc29f058e9c9 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.8-rc.4", + "version": "0.2.9", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/api-client/CHANGELOG.md b/ee/packages/api-client/CHANGELOG.md index 8369eeee332f..cc600456d258 100644 --- a/ee/packages/api-client/CHANGELOG.md +++ b/ee/packages/api-client/CHANGELOG.md @@ -1,6 +1,34 @@ # @rocket.chat/api-client -## 0.1.8-rc.4 +## 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 diff --git a/ee/packages/api-client/package.json b/ee/packages/api-client/package.json index 1fe0aaf539d0..4827eddb734a 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.8-rc.4", + "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 dd1327fb6042..f623b9a729e6 100644 --- a/ee/packages/ddp-client/CHANGELOG.md +++ b/ee/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,34 @@ # @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 @@ -48,6 +77,13 @@ - @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 diff --git a/ee/packages/ddp-client/package.json b/ee/packages/ddp-client/package.json index 0c64d9117894..5f62047c2429 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.2.0-rc.4", + "version": "0.2.0", "devDependencies": { "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", diff --git a/ee/packages/license/__tests__/MockedLicenseBuilder.ts b/ee/packages/license/__tests__/MockedLicenseBuilder.ts index 316261744da5..4f2b49596be3 100644 --- a/ee/packages/license/__tests__/MockedLicenseBuilder.ts +++ b/ee/packages/license/__tests__/MockedLicenseBuilder.ts @@ -197,7 +197,6 @@ export class MockedLicenseBuilder { export const getReadyLicenseManager = async () => { const license = new LicenseImp(); await license.setWorkspaceUrl('http://localhost:3000'); - await license.setWorkspaceUrl('http://localhost:3000'); license.setLicenseLimitCounter('activeUsers', () => 0); license.setLicenseLimitCounter('guestUsers', () => 0); diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts index 4c7c5a8255d1..6147d12623bc 100644 --- a/ee/packages/license/__tests__/emitter.spec.ts +++ b/ee/packages/license/__tests__/emitter.spec.ts @@ -63,4 +63,57 @@ describe('Event License behaviors', () => { 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 index 962f591750ad..35a7a495edc0 100644 --- a/ee/packages/license/__tests__/setLicense.spec.ts +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -14,7 +14,7 @@ const VALID_LICENSE = describe('License set license procedures', () => { describe('Invalid formats', () => { - it('by default it should have no license', async () => { + it('should have no license by default', async () => { const license = new LicenseImp(); expect(license.hasValidLicense()).toBe(false); @@ -39,7 +39,7 @@ describe('License set license procedures', () => { await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); }); - it('should keep a valid license if a new invalid license is applied', async () => { + 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); @@ -99,5 +99,54 @@ describe('License set license procedures', () => { 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/package.json b/ee/packages/license/package.json index f6a1e7a2b7d5..6810f53e40dd 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -11,6 +11,7 @@ "@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", @@ -42,6 +43,7 @@ "dependencies": { "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/jwt": "workspace:^", - "@rocket.chat/logger": "workspace:^" + "@rocket.chat/logger": "workspace:^", + "bcrypt": "^5.0.1" } } diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts index b6d52bbfa8c5..8b5af5f3c481 100644 --- a/ee/packages/license/src/definition/LicenseBehavior.ts +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -1,8 +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[]; -}; +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/LicenseValidationOptions.ts b/ee/packages/license/src/definition/LicenseValidationOptions.ts new file mode 100644 index 000000000000..6aa1e4213c62 --- /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 index a2c44744bd75..9dfc6d36be7f 100644 --- a/ee/packages/license/src/definition/LimitContext.ts +++ b/ee/packages/license/src/definition/LimitContext.ts @@ -2,4 +2,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { LicenseLimitKind } from './ILicenseV3'; -export type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; +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 000000000000..53f3afe846db --- /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/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 9d4025e4bce3..9256bcafe5f7 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,4 +1,4 @@ -import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; import type { LicenseModule } from '../definition/LicenseModule'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; @@ -21,10 +21,45 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) { } } -export function limitReached(this: LicenseManager, limitKind: LicenseLimitKind) { +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:${limitKind}`); + 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 index d6e9fb016f2c..ecabecb28c0f 100644 --- a/ee/packages/license/src/events/listeners.ts +++ b/ee/packages/license/src/events/listeners.ts @@ -1,4 +1,5 @@ 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'; @@ -58,18 +59,26 @@ export function onToggledFeature( }; } -export function onModule(this: LicenseManager, cb: (...args: any[]) => void) { +export function onModule(this: LicenseManager, cb: (data: { module: LicenseModule; valid: boolean }) => void) { this.on('module', cb); } -export function onValidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { +export function onValidateLicense(this: LicenseManager, cb: () => void) { this.on('validate', cb); } -export function onInvalidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { +export function onInvalidateLicense(this: LicenseManager, cb: () => void) { this.on('invalidate', cb); } -export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: (...args: any[]) => void) { +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/index.ts b/ee/packages/license/src/index.ts index c5dbd9f9496f..77e2976f156a 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -4,6 +4,7 @@ import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; import { + onBehaviorTriggered, onInvalidFeature, onInvalidateLicense, onLimitReached, @@ -45,12 +46,13 @@ interface License { 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; - inFairPolicy: boolean; }>; // Deprecated: @@ -78,8 +80,8 @@ export class LicenseImp extends LicenseManager implements License { getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; - public async isLimitReached(action: T, context?: Partial>) { - return this.shouldPreventAction(action, context, 0); + public async isLimitReached(action: T, context?: Partial>): Promise { + return this.shouldPreventAction(action, 0, context); } onValidFeature = onValidFeature; @@ -96,6 +98,8 @@ export class LicenseImp extends LicenseManager implements License { onLimitReached = onLimitReached; + onBehaviorTriggered = onBehaviorTriggered; + // Deprecated: onLicense = onLicense; diff --git a/ee/packages/license/src/isItemAllowed.ts b/ee/packages/license/src/isItemAllowed.ts new file mode 100644 index 000000000000..16787cdf9c4d --- /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 index 36744585d59f..989be7b69ae1 100644 --- a/ee/packages/license/src/license.spec.ts +++ b/ee/packages/license/src/license.spec.ts @@ -40,3 +40,156 @@ it('should prevent if the counter is equal or over the limit', async () => { 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 index a420eb2b0d57..f2d8eef362bd 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -1,13 +1,17 @@ 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'; @@ -15,6 +19,7 @@ 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'; @@ -22,17 +27,15 @@ import { isReadyForValidation } from './validation/isReadyForValidation'; import { runValidation } from './validation/runValidation'; import { validateFormat } from './validation/validateFormat'; -export class LicenseManager extends Emitter< - Record<`limitReached:${LicenseLimitKind}` | `${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { - validate: undefined; - invalidate: undefined; - module: { module: LicenseModule; valid: boolean }; - } -> { +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; @@ -43,10 +46,14 @@ export class LicenseManager extends Emitter< private _valid: boolean | undefined; - private _inFairPolicy: 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; } @@ -59,10 +66,6 @@ export class LicenseManager extends Emitter< return this._valid; } - public get inFairPolicy(): boolean { - return Boolean(this._inFairPolicy); - } - public async setWorkspaceUrl(url: string) { this.workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); @@ -75,15 +78,34 @@ export class LicenseManager extends Emitter< 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._inFairPolicy = 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): Promise { const hadValidLicense = this.hasValidLicense(); this.clearLicenseData(); @@ -92,13 +114,15 @@ export class LicenseManager extends Emitter< this._unmodifiedLicense = originalLicense || newLicense; this._license = newLicense; - await this.validateLicense(); - + const isNewLicense = encryptedLicense !== this._lockedLicense; this._lockedLicense = encryptedLicense; - } finally { - if (hadValidLicense && !this.hasValidLicense()) { - this.emit('invalidate'); - invalidateAll.call(this); + + await this.validateLicense({ isNewLicense }); + } catch (e) { + if (e instanceof InvalidLicenseError) { + if (hadValidLicense) { + this.invalidateLicense(); + } } } } @@ -111,7 +135,7 @@ export class LicenseManager extends Emitter< return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); } - private async validateLicense(): Promise { + private async validateLicense(options: LicenseValidationOptions = {}): Promise { if (!this._license) { throw new InvalidLicenseError(); } @@ -120,15 +144,40 @@ export class LicenseManager extends Emitter< throw new NotReadyForValidation(); } - // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation.call(this, this._license, [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]); + 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); + } - this.processValidationResult(validationResult); + 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): Promise { @@ -175,29 +224,10 @@ export class LicenseManager extends Emitter< } } - private processValidationResult(result: BehaviorWithContext[]): void { - if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { - return; - } - - this._valid = true; - this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); - - if (this._license.information.tags) { - replaceTags(this._license.information.tags); + private triggerBehaviorEvents(validationResult: BehaviorWithContext[]): void { + for (const { ...options } of validationResult) { + behaviorTriggered.call(this, { ...options }); } - - const disabledModules = getModulesToDisable(result); - const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - - replaceModules.call( - this, - modulesToEnable.map(({ module }) => module), - ); - logger.log({ msg: 'License validated', modules: modulesToEnable }); - - this.emit('validate'); - showLicense.call(this, this._license, this._valid); } public hasValidLicense(): boolean { @@ -212,27 +242,47 @@ export class LicenseManager extends Emitter< public async shouldPreventAction( action: T, - context?: Partial>, - newCount = 1, + extraCount = 0, + context: Partial> = {}, + { suppressLog }: Pick = {}, ): Promise { const license = this.getLicense(); if (!license) { return false; } - const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + newCount; - return Boolean( - license.limits[action] - ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) - .some(({ max }) => max < currentValue), - ); + 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; - inFairPolicy: boolean; }> { const activeModules = getModules.call(this); const license = this.getLicense(); @@ -241,7 +291,7 @@ export class LicenseManager extends Emitter< const limits = ( (license && (await Promise.all( - (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) + globalLimitKinds .map((limitKey) => ({ limitKey, max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), @@ -263,7 +313,6 @@ export class LicenseManager extends Emitter< license, activeModules, limits: limits as Record, - inFairPolicy: this.inFairPolicy, }; } } diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index 7570ec525fc7..6931fb7a6a5d 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -29,7 +29,8 @@ export function hasModule(this: LicenseManager, module: LicenseModule) { return this.modules.has(module); } -export function replaceModules(this: LicenseManager, newModules: LicenseModule[]) { +export function replaceModules(this: LicenseManager, newModules: LicenseModule[]): boolean { + let anyChange = false; for (const moduleName of newModules) { if (this.modules.has(moduleName)) { continue; @@ -37,6 +38,7 @@ export function replaceModules(this: LicenseManager, newModules: LicenseModule[] this.modules.add(moduleName); moduleValidated.call(this, moduleName); + anyChange = true; } for (const moduleName of this.modules) { @@ -46,5 +48,8 @@ export function replaceModules(this: LicenseManager, newModules: LicenseModule[] moduleRemoved.call(this, moduleName); this.modules.delete(moduleName); + anyChange = true; } + + return anyChange; } diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts index ca2639678475..33434cae116d 100644 --- a/ee/packages/license/src/tags.ts +++ b/ee/packages/license/src/tags.ts @@ -1,23 +1,24 @@ import type { ILicenseTag } from './definition/ILicenseTag'; +import { type LicenseManager } from './license'; -export const tags = new Set(); - -export const addTag = (tag: ILicenseTag) => { +export function addTag(this: LicenseManager, tag: ILicenseTag) { // make sure to not add duplicated tag names - for (const addedTag of tags) { + for (const addedTag of this.tags) { if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { return; } } - tags.add(tag); -}; + this.tags.add(tag); +} -export const replaceTags = (newTags: ILicenseTag[]) => { - tags.clear(); +export function replaceTags(this: LicenseManager, newTags: ILicenseTag[]) { + this.tags.clear(); for (const tag of newTags) { - addTag(tag); + addTag.call(this, tag); } -}; +} -export const getTags = () => [...tags]; +export function getTags(this: LicenseManager) { + return [...this.tags]; +} diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts index 7586f54c8c54..10681cf04b47 100644 --- a/ee/packages/license/src/v2/convertToV3.ts +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -36,7 +36,7 @@ export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { serverUrls: [ { value: v2.url, - type: 'url', + type: 'regex', }, ], validPeriods: [ diff --git a/ee/packages/license/src/validation/filterBehaviorsResult.ts b/ee/packages/license/src/validation/filterBehaviorsResult.ts new file mode 100644 index 000000000000..e51dbac20a53 --- /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 index 88cedc6c7bc9..8f9c6ed4034e 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -1,12 +1,9 @@ -import type { IUser } from '@rocket.chat/core-typings'; - 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'; -type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; - export function setLicenseLimitCounter( this: LicenseManager, limitKey: T, diff --git a/ee/packages/license/src/validation/getResultingBehavior.ts b/ee/packages/license/src/validation/getResultingBehavior.ts index 47e2d91b8b89..22ca02bfd220 100644 --- a/ee/packages/license/src/validation/getResultingBehavior.ts +++ b/ee/packages/license/src/validation/getResultingBehavior.ts @@ -1,8 +1,12 @@ +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): BehaviorWithContext => { +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) { @@ -10,11 +14,15 @@ export const getResultingBehavior = (data: LicenseLimit | LicensePeriod | Partia 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/runValidation.spec.ts b/ee/packages/license/src/validation/runValidation.spec.ts index 98797c86cd27..523090acd63a 100644 --- a/ee/packages/license/src/validation/runValidation.spec.ts +++ b/ee/packages/license/src/validation/runValidation.spec.ts @@ -22,16 +22,16 @@ describe('Validation behaviors', () => { }); await expect( - runValidation.call(licenseManager, await license.build(), [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]), + 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 index 9cb623b8eae0..922b4c49162e 100644 --- a/ee/packages/license/src/validation/runValidation.ts +++ b/ee/packages/license/src/validation/runValidation.ts @@ -1,5 +1,6 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; +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'; @@ -8,15 +9,11 @@ import { validateLicenseUrl } from './validateLicenseUrl'; export async function runValidation( this: LicenseManager, license: ILicenseV3, - behaviorsToValidate: LicenseBehavior[] = [], + options: LicenseValidationOptions, ): Promise { - const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate.length || behaviorsToValidate.includes(behavior); - return [ - ...new Set([ - ...validateLicenseUrl.call(this, license, shouldValidateBehavior), - ...validateLicensePeriods(license, shouldValidateBehavior), - ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior)), - ]), + ...validateLicenseUrl.call(this, license, options), + ...validateLicensePeriods(license, options), + ...(await validateLicenseLimits.call(this, license, options)), ]; } diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 168effe6a250..f321252ba573 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -1,5 +1,7 @@ -import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +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'; @@ -8,30 +10,49 @@ import { getResultingBehavior } from './getResultingBehavior'; export async function validateLicenseLimits( this: LicenseManager, license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, + options: LicenseValidationOptions, ): Promise { const { limits } = license; - const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; + 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 && behaviorFilter(behavior)); + const limitList = limits[limitKey]?.filter(({ behavior, max }) => max >= 0 && isBehaviorAllowed(behavior, options)); if (!limitList?.length) { return []; } - const currentValue = await getCurrentValueForLicenseLimit.call(this, limitKey); + const extraCount = options.context?.[limitKey]?.extraCount ?? 0; + const currentValue = (await getCurrentValueForLicenseLimit.call(this, limitKey, options.context?.[limitKey])) + extraCount; + return limitList - .filter(({ max }) => max < currentValue) + .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) => { - logger.error({ - msg: 'Limit validation failed', - kind: limitKey, - limit, - }); - return getResultingBehavior(limit); + if (!options.suppressLog) { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + } + return getResultingBehavior(limit, { reason: 'limit', limit: limitKey }); }); }), ) diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts index 5b3fae433e38..fb27f72d0a8e 100644 --- a/ee/packages/license/src/validation/validateLicensePeriods.ts +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -1,6 +1,8 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +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'; @@ -18,21 +20,23 @@ export const isPeriodInvalid = (from: Timestamp | undefined, until: Timestamp | return false; }; -export const validateLicensePeriods = ( - license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, -): BehaviorWithContext[] => { +export const validateLicensePeriods = (license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] => { const { validation: { validPeriods }, } = license; return validPeriods - .filter(({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && isPeriodInvalid(validFrom, validUntil)) + .filter( + ({ validFrom, validUntil, invalidBehavior }) => isBehaviorAllowed(invalidBehavior, options) && isPeriodInvalid(validFrom, validUntil), + ) .map((period) => { - logger.error({ - msg: 'Period validation failed', - period, - }); - return getResultingBehavior(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 000000000000..9047876f8fbc --- /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 index 55cd076c4378..416b107511cb 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -1,10 +1,14 @@ +import crypto from 'crypto'; + import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +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'; -export const validateUrl = (licenseURL: string, url: string) => { +const validateRegex = (licenseURL: string, url: string) => { licenseURL = licenseURL .replace(/\./g, '\\.') // convert dots to literal .replace(/\*/g, '.*'); // convert * to .* @@ -13,12 +17,17 @@ export const validateUrl = (licenseURL: string, url: string) => { return !!regex.exec(url); }; -export function validateLicenseUrl( - this: LicenseManager, - license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, -): BehaviorWithContext[] { - if (!behaviorFilter('invalidate_license')) { +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 []; } @@ -30,18 +39,16 @@ export function validateLicenseUrl( if (!workspaceUrl) { logger.error('Unable to validate license URL without knowing the workspace URL.'); - return [getResultingBehavior({ behavior: 'invalidate_license' })]; + return [getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' })]; } return serverUrls .filter((url) => { switch (url.type) { case 'regex': - // #TODO - break; + return !validateRegex(url.value, workspaceUrl); case 'hash': - // #TODO - break; + return !validateHash(url.value, workspaceUrl); case 'url': return !validateUrl(url.value, workspaceUrl); } @@ -49,11 +56,13 @@ export function validateLicenseUrl( return false; }) .map((url) => { - logger.error({ - msg: 'Url validation failed', - url, - workspaceUrl, - }); - return getResultingBehavior({ behavior: 'invalidate_license' }); + if (!options.suppressLog) { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + } + return getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' }); }); } diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 5d4f0468c000..6e4556b12ef0 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,49 @@ # @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 diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 17a71abc3da5..e5875c054eef 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.14-rc.4", + "version": "0.0.15", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index ce21e963911b..0e135d5ed263 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -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/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 13dc1a43e0f5..1da194044df7 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,26 @@ # @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 diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index daa5105e3fdc..9081c64fba34 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.14-rc.4", + "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 befd96b837ee..4ee4d1a7df52 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,33 @@ # @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 diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index fdb6a16393b3..da77719abc88 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.0.14-rc.4", + "version": "0.0.15", "private": true, "devDependencies": { "@babel/core": "~7.22.9", diff --git a/ee/packages/ui-theming/CHANGELOG.md b/ee/packages/ui-theming/CHANGELOG.md index 746a406ad105..134f170ce82a 100644 --- a/ee/packages/ui-theming/CHANGELOG.md +++ b/ee/packages/ui-theming/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-theming +## 0.1.0 + +### Minor Changes + +- 357a3a50fa: feat: high-contrast theme + ## 0.1.0-rc.0 ### Minor Changes diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index f15c94365c06..9508e2b8e41e 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-theming", - "version": "0.1.0-rc.0", + "version": "0.1.0", "private": true, "devDependencies": { "@rocket.chat/css-in-js": "next", diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 4825beec0cff..39f8d3f2bfed 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -44,7 +44,7 @@ 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' }, diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 0f067001c3be..940c05513ec1 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,40 @@ # @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 @@ -57,6 +92,14 @@ - @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 diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 3492cc1f77bf..7f6a82642d47 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.2.0-rc.4", + "version": "0.2.0", "private": true, "devDependencies": { "@babel/core": "~7.22.9", diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index d9eee82029af..f7be69ce2a7c 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> { diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index be66b7a23ccb..f6c174da450f 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,27 @@ # @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 @@ -23,6 +45,8 @@ - 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 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2b673be0f857..60874e11c810 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,14 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.4.0-rc.4", + "version": "6.4.0", + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "eslint": "~8.45.0", + "mongodb": "^4.17.1", + "prettier": "~2.8.8", + "typescript": "~5.2.2" + }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", @@ -20,13 +27,6 @@ "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "^0.32.1" }, - "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", - "eslint": "~8.45.0", - "mongodb": "^4.17.1", - "prettier": "~2.8.8", - "typescript": "~5.2.2" - }, "volta": { "extends": "../../package.json" } diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts index d22ea36aa7c6..e80d63ab15d0 100644 --- a/packages/core-typings/src/ILivechatVisitor.ts +++ b/packages/core-typings/src/ILivechatVisitor.ts @@ -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/IRoom.ts b/packages/core-typings/src/IRoom.ts index 875dea70781e..523450e9594d 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; diff --git a/packages/core-typings/src/ISetting.ts b/packages/core-typings/src/ISetting.ts index 0766d782980c..7b3aaa5cf2a4 100644 --- a/packages/core-typings/src/ISetting.ts +++ b/packages/core-typings/src/ISetting.ts @@ -72,7 +72,7 @@ export interface ISettingBase { hidden?: boolean; modules?: Array; 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 6bbc2da81b74..443cbfb23957 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -3,6 +3,7 @@ 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; @@ -16,6 +17,8 @@ export interface IStats { registerServer?: boolean; }; uniqueId: string; + deploymentFingerprintHash: string; + deploymentFingerprintVerified: boolean; installedAt?: string; version?: string; tag?: string; @@ -93,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; diff --git a/packages/core-typings/src/migrations/IControl.ts b/packages/core-typings/src/migrations/IControl.ts index 9ff993703550..3f89ce730f1a 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 703cf3b4ca77..c6235175dafc 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 000000000000..8591edbb0287 --- /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 32872b06ffbe..b0cf975ee4d8 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,33 @@ # @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 @@ -51,6 +79,13 @@ ### 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. diff --git a/packages/cron/package.json b/packages/cron/package.json index c7db1e371cc1..37e751fe03d5 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.0.10-rc.4", + "version": "0.0.11", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md index 59cacad3f786..62704f6c7714 100644 --- a/packages/eslint-config/CHANGELOG.md +++ b/packages/eslint-config/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/eslint-config +## 0.6.0 + +### Minor Changes + +- 0f56aacc4d: Unpublished changes in ESLint config + ## 0.6.0-rc.0 ### Minor Changes diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 1ef1e593f7d5..9b0adef2d80e 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/eslint-config", - "version": "0.6.0-rc.0", + "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 bffaa90dcb07..dd8a0fce9789 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,32 @@ # 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 @@ -51,6 +78,14 @@ - @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 diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index af89df400371..83ab677e2a9f 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": "2.0.0-rc.4", + "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": "0.6.0-rc.0", + "@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": "2.0.0-rc.4", + "@rocket.chat/ui-contexts": "2.0.0", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "2.0.0-rc.4", + "@rocket.chat/ui-video-conf": "2.0.0", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index d0178b78e124..336e44b65402 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,33 @@ # @rocket.chat/gazzodown +## 2.0.0 + +### Patch Changes + +- Updated dependencies [239a34e877] +- Updated dependencies [203304782f] +- Updated dependencies [4186eecf05] +- Updated dependencies [ba24f3c21f] +- Updated dependencies [ebab8c4dd8] +- Updated dependencies [074db3b419] +- Updated dependencies [1041d4d361] +- Updated dependencies [61128364d6] +- Updated dependencies [d45365436e] +- Updated dependencies [b8f3d5014f] +- Updated dependencies [ee3815fce4] + - @rocket.chat/core-typings@6.4.0 + - @rocket.chat/ui-client@2.0.0 + - @rocket.chat/ui-contexts@2.0.0 + +## 2.0.0-rc.5 + +### Patch Changes + +- Updated dependencies [1041d4d361] + - @rocket.chat/core-typings@6.4.0-rc.5 + - @rocket.chat/ui-contexts@2.0.0-rc.5 + - @rocket.chat/ui-client@2.0.0-rc.5 + ## 2.0.0-rc.4 ### Patch Changes @@ -50,6 +78,14 @@ - @rocket.chat/ui-client@2.0.0-rc.0 - @rocket.chat/ui-contexts@2.0.0-rc.0 +## 1.0.8 + +### Patch Changes + +- @rocket.chat/core-typings@6.3.8 +- @rocket.chat/ui-contexts@1.0.8 +- @rocket.chat/ui-client@1.0.8 + ## 1.0.7 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index bf46cd4592ce..89136a296f9d 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "2.0.0-rc.4", + "version": "2.0.0", "private": true, "devDependencies": { "@babel/core": "~7.22.9", @@ -65,14 +65,14 @@ "/dist" ], "peerDependencies": { - "@rocket.chat/core-typings": "6.4.0-rc.4", + "@rocket.chat/core-typings": "6.4.0", "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "2.0.0-rc.4", - "@rocket.chat/ui-contexts": "2.0.0-rc.4", + "@rocket.chat/ui-client": "2.0.0", + "@rocket.chat/ui-contexts": "2.0.0", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 88d629fae65c..7eecb2d845e9 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/i18n +## 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. + ## 0.0.2-rc.0 ### Patch Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index da2dae7600ac..08c924493580 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "0.0.2-rc.0", + "version": "0.0.2", "private": true, "devDependencies": { "@babel/core": "~7.22.10", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 3571dc6806ce..3c454da0e972 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/instance-status +## 0.0.15 + +### Patch Changes + +- @rocket.chat/models@0.0.15 + +## 0.0.15-rc.5 + +### Patch Changes + +- @rocket.chat/models@0.0.15-rc.5 + ## 0.0.14-rc.4 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index ed9c13ba5231..4b6d8a882650 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.0.14-rc.4", + "version": "0.0.15", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 922a2d7f442b..7d537677ad04 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,25 @@ # @rocket.chat/livechat Change Log +## 1.14.0 + +### Minor Changes + +- 04fe492555: Added new Omnichannel's trigger condition "After starting a chat". + +### Patch Changes + +- 62a13ed97d: chore: (Livechat) Replace all `dangerouslySetInnerHTML` with `gazzodown` +- 817141dcab: fix: Issue caused by spaces in the `config.url` setting +- 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. + - @rocket.chat/gazzodown@2.0.0 + - @rocket.chat/random@1.2.1 + +## 1.14.0-rc.5 + +### Patch Changes + +- @rocket.chat/gazzodown@2.0.0-rc.5 + ## 1.14.0-rc.4 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index deba850e4abd..756248c1df0c 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.14.0-rc.4", + "version": "1.14.0", "files": [ "/build" ], diff --git a/packages/livechat/src/lib/random.ts b/packages/livechat/src/lib/random.ts index 705fdcacc7da..068c84c1c459 100644 --- a/packages/livechat/src/lib/random.ts +++ b/packages/livechat/src/lib/random.ts @@ -4,6 +4,6 @@ export const chooseElement = Random.choice; export const createRandomString = Random._randomString; -export const createRandomId = Random.id; +export const createRandomId = () => Random.id(); export const createToken = () => Random.hexString(64); diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 448ae0b742a4..915bcabc5395 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,13 @@ # @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 diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index d5bc013d9a18..c2aeb2665350 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.0.2-rc.0", + "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 f80a0d805977..8d03eaafa208 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,39 @@ # @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 @@ -50,6 +84,12 @@ - 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 diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index b187c15b99c7..5174bf22f97a 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.1.0-rc.4", + "version": "0.1.0", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 68b72be33ba8..20100cbb4f61 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,11 @@ -import type { IMessage, IOmnichannelRoom, IOmnichannelRoomClosingInfo, ISetting, ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { + IMessage, + IOmnichannelRoom, + IOmnichannelRoomClosingInfo, + ISetting, + ILivechatVisitor, + MACStats, +} from '@rocket.chat/core-typings'; import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter } from 'mongodb'; import type { FindPaginated } from '..'; @@ -234,4 +241,7 @@ export interface ILivechatRoomsModel extends IBaseModel { setVisitorInactivityInSecondsById(roomId: string, visitorInactivity: any): Promise; changeVisitorByRoomId(roomId: string, visitor: { _id: string; username: string; token: string }): Promise; unarchiveOneById(roomId: string): Promise; + markVisitorActiveForPeriod(rid: string, period: string): Promise; + getMACStatisticsForPeriod(period: string): Promise; + getMACStatisticsBetweenDates(start: Date, end: Date): Promise; } diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index 370db511dadf..5c598c6a6a97 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -48,4 +48,16 @@ export interface ILivechatVisitorsModel extends IBaseModel { updateById(_id: string, update: UpdateFilter): Promise; saveGuestEmailPhoneById(_id: string, emails: string[], phones: string[]): Promise; + + isVisitorActiveOnPeriod(visitorId: string, period: string): Promise; + + markVisitorActiveForPeriod(visitorId: string, period: string): Promise; + + findOneEnabledById(_id: string, options?: FindOptions): Promise; + + disableById(_id: string): Promise; + + findEnabled(query: Filter, options?: FindOptions): FindCursor; + + countVisitorsOnPeriod(period: string): Promise; } diff --git a/packages/model-typings/src/models/ISettingsModel.ts b/packages/model-typings/src/models/ISettingsModel.ts index 9dc2005867fa..d382d4853a4b 100644 --- a/packages/model-typings/src/models/ISettingsModel.ts +++ b/packages/model-typings/src/models/ISettingsModel.ts @@ -17,6 +17,11 @@ export interface ISettingsModel extends IBaseModel { value: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, ): Promise; + resetValueById( + _id: string, + value?: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, + ): Promise; + incrementValueById(_id: ISetting['_id'], value?: number): Promise; updateOptionsById( diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index aebda87c78cb..53b0a69ec232 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -1,5 +1,15 @@ -import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb'; +import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, AtLeast } from '@rocket.chat/core-typings'; +import type { + FindOptions, + FindCursor, + UpdateResult, + DeleteResult, + Document, + AggregateOptions, + Filter, + InsertOneResult, + InsertManyResult, +} from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -216,6 +226,10 @@ export interface ISubscriptionsModel extends IBaseModel { ): Promise; removeByUserId(userId: string): Promise; createWithRoomAndUser(room: IRoom, user: IUser, extraData?: Record): Promise>; + createWithRoomAndManyUsers( + room: IRoom, + users: { user: AtLeast; extraData: Record }[], + ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; removeByRoomIdAndUserId(roomId: string, userId: string): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 1ee2a432c3df..f14f5bc90d0d 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -239,6 +239,7 @@ export interface IUsersModel extends IBaseModel { removeAllRoomsByUserId(userId: string): Promise; removeRoomByUserId(userId: string, rid: string): Promise; addRoomByUserId(userId: string, rid: string): Promise; + addRoomByUserIds(uids: string[], rid: string): Promise; removeRoomByRoomIds(rids: string[]): Promise; getLoginTokensByUserId(userId: string): FindCursor; addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise; @@ -317,7 +318,7 @@ export interface IUsersModel extends IBaseModel { findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions): FindCursor; findCrowdUsers(options?: FindOptions): FindCursor; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; + findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions): FindCursor; diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 31d2440aaa7f..ac261acbc4d7 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,24 @@ # @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 diff --git a/packages/models/package.json b/packages/models/package.json index 4dbaa73f037b..1644a8362c77 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.0.14-rc.4", + "version": "0.0.15", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/random/src/NodeRandomGenerator.ts b/packages/random/src/NodeRandomGenerator.ts index b9d556c6ac07..8c9f239413ca 100644 --- a/packages/random/src/NodeRandomGenerator.ts +++ b/packages/random/src/NodeRandomGenerator.ts @@ -38,6 +38,16 @@ export class NodeRandomGenerator extends RandomGenerator { return result.substring(0, digits); } + /** + * @name Random.between Returns a random integer between min and max, inclusive. + * @param min Minimum value (inclusive) + * @param max Maximum value (inclusive) + * @returns A random integer between min and max, inclusive. + */ + between(min: number, max: number) { + return Math.floor(this.fraction() * (max - min + 1)) + min; + } + protected safelyCreateWithSeeds(...seeds: readonly unknown[]) { return new AleaRandomGenerator({ seeds }); } diff --git a/packages/release-action/CHANGELOG.md b/packages/release-action/CHANGELOG.md index 23416df8c835..3435ec55ffa5 100644 --- a/packages/release-action/CHANGELOG.md +++ b/packages/release-action/CHANGELOG.md @@ -1,5 +1,16 @@ # @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 diff --git a/packages/release-action/package.json b/packages/release-action/package.json index c227df3f2d0f..0a1e3ded8927 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/release-action", - "version": "2.2.0-rc.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 506ed5c3580f..da3d48464546 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,43 @@ # @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 @@ -49,6 +87,12 @@ - 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 diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 9da7694d28b9..d5996987acf1 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.4.0-rc.4", + "version": "6.4.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/jest": "~29.5.3", diff --git a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts index c8bb88cfc1c6..e25dfb0ce2fb 100644 --- a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts +++ b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts @@ -12,6 +12,7 @@ export type ChannelsCreateProps = { encrypted?: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const channelsCreatePropsSchema = { diff --git a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts index c34a720bd4b7..7c3781d787c4 100644 --- a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts @@ -14,6 +14,7 @@ export type GroupsCreateProps = { encrypted: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const GroupsCreatePropsSchema = { diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 6dc935aae739..87c0106f6d3f 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -49,7 +49,6 @@ export type LicensesEndpoints = { license: ILicenseV3 | undefined; activeModules: string[]; limits: Record; - inFairPolicy: boolean; }; }; }; diff --git a/packages/rest-typings/src/v1/misc.ts b/packages/rest-typings/src/v1/misc.ts index 4af37334e287..804b72a763de 100644 --- a/packages/rest-typings/src/v1/misc.ts +++ b/packages/rest-typings/src/v1/misc.ts @@ -164,6 +164,22 @@ const MethodCallAnonSchema = { export const isMethodCallAnonProps = ajv.compile(MethodCallAnonSchema); +type Fingerprint = { setDeploymentAs: 'new-workspace' | 'updated-configuration' }; + +const FingerprintSchema = { + type: 'object', + properties: { + setDeploymentAs: { + type: 'string', + enum: ['new-workspace', 'updated-configuration'], + }, + }, + required: ['setDeploymentAs'], + additionalProperties: false, +}; + +export const isFingerprintProps = ajv.compile(FingerprintSchema); + type PwGetPolicyReset = { token: string }; const PwGetPolicyResetSchema = { @@ -229,6 +245,12 @@ export type MiscEndpoints = { }; }; + '/v1/fingerprint': { + POST: (params: Fingerprint) => { + success: boolean; + }; + }; + '/v1/smtp.check': { GET: () => { isSMTPConfigured: boolean; diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md new file mode 100644 index 000000000000..b5d9e6f419d4 --- /dev/null +++ b/packages/tools/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/tools + +## 0.1.0 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations + +## 0.1.0-rc.0 + +### Minor Changes + +- 1041d4d361: Added option to select between two script engine options for the integrations diff --git a/packages/tools/package.json b/packages/tools/package.json index d9ab70550ce4..ed5e7bdd44bf 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/tools", - "version": "0.0.1", + "version": "0.1.0", "private": true, "devDependencies": { "@types/jest": "~29.5.3", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 5a987f493cf1..77ab6ff504ca 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,25 @@ # @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 @@ -38,6 +58,12 @@ - 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 diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 7df166d9e283..fa227575ccc2 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "2.0.0-rc.4", + "version": "2.0.0", "private": true, "devDependencies": { "@babel/core": "~7.22.9", @@ -61,7 +61,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "2.0.0-rc.4", + "@rocket.chat/ui-contexts": "2.0.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index 1420a62346d6..6c5e12be8622 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -57,7 +57,6 @@ type DropDownProps = { selectedOptionsTitle: TranslationKey; selectedOptions: OptionProp[]; setSelectedOptions: Dispatch>; - customSetSelected: Dispatch>; searchBarText?: TranslationKey; }; @@ -67,7 +66,6 @@ export const MultiSelectCustom = ({ selectedOptionsTitle, selectedOptions, setSelectedOptions, - customSetSelected, searchBarText, }: DropDownProps): ReactElement => { const reference = useRef(null); @@ -90,26 +88,15 @@ export const MultiSelectCustom = ({ const onSelect = (item: OptionProp, e?: FormEvent): void => { e?.stopPropagation(); - item.checked = !item.checked; if (item.checked === true) { - // the user has enabled this option -> add it to the selected options setSelectedOptions([...new Set([...selectedOptions, item])]); - customSetSelected((prevItems) => { - const newItems = prevItems; - const toggledItem = newItems.find(({ id }) => id === item.id); - - if (toggledItem) { - toggledItem.checked = !toggledItem.checked; - } - - return [...prevItems]; - }); - } else { - // the user has disabled this option -> remove this from the selected options list - setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id)); + return; } + + // the user has disabled this option -> remove this from the selected options list + setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id)); }; const count = dropdownOptions.filter((option) => option.checked).length; diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index 7e6bfdb9fee1..d8f8d60d8096 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -2,7 +2,7 @@ import { Box, CheckBox, Icon, Option, SearchInput, Tile } from '@rocket.chat/fus import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; -import { Fragment, useCallback, useEffect, useState } from 'react'; +import { Fragment, useCallback, useState } from 'react'; import type { OptionProp } from './MultiSelectCustom'; import { useFilteredOptions } from './useFilteredOptions'; @@ -19,13 +19,10 @@ const MultiSelectCustomList = ({ const t = useTranslation(); const [text, setText] = useState(''); - const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - - const [optionSearch, setOptionSearch] = useState(''); - useEffect(() => setOptionSearch(text), [setOptionSearch, text]); + const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - const filteredOptions = useFilteredOptions(optionSearch, options); + const filteredOptions = useFilteredOptions(text, options); return ( @@ -48,11 +45,11 @@ const MultiSelectCustomList = ({ {t(option.text as TranslationKey)}
) : ( -