diff --git a/.changeset/afraid-parents-bake.md b/.changeset/afraid-parents-bake.md new file mode 100644 index 0000000000000..af2b8a10d4899 --- /dev/null +++ b/.changeset/afraid-parents-bake.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +REST endpoint `/v1/users.createToken` is not deprecated anymore. It now requires a `secret` parameter to generate a token for a user. This change is part of the effort to enhance security by ensuring that tokens are generated with an additional layer of validation. The `secret` parameter is validated against a new environment variable `CREATE_TOKENS_FOR_USERS_SECRET`. diff --git a/.changeset/beige-seahorses-reply.md b/.changeset/beige-seahorses-reply.md new file mode 100644 index 0000000000000..bf87249b57e07 --- /dev/null +++ b/.changeset/beige-seahorses-reply.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/web-ui-registration": major +--- + +Removes the deprecated sendConfirmationEmail method diff --git a/.changeset/blue-beans-check.md b/.changeset/blue-beans-check.md new file mode 100644 index 0000000000000..f1d66153e3f1f --- /dev/null +++ b/.changeset/blue-beans-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:removeUnit` method diff --git a/.changeset/breezy-timers-flow.md b/.changeset/breezy-timers-flow.md new file mode 100644 index 0000000000000..5894d387b05de --- /dev/null +++ b/.changeset/breezy-timers-flow.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes the deprecated meteor method: `livechat:saveTag` diff --git a/.changeset/brown-carrots-bathe.md b/.changeset/brown-carrots-bathe.md new file mode 100644 index 0000000000000..9e39e63edc9b9 --- /dev/null +++ b/.changeset/brown-carrots-bathe.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/rest-typings': patch +--- + +Adds deprecation warning for `livechat:removeMonitor` and new endpoint replacing it; `livechat/monitor.remove` diff --git a/.changeset/brown-llamas-worry.md b/.changeset/brown-llamas-worry.md new file mode 100644 index 0000000000000..9cbf74ba7317f --- /dev/null +++ b/.changeset/brown-llamas-worry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Adds a deprecation warning for `livechat:saveBusinessHour` and new endpoint replacing it; `livechat/business-hours.save` diff --git a/.changeset/bump-patch-1766456337926.md b/.changeset/bump-patch-1766456337926.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1766456337926.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1767191875055.md b/.changeset/bump-patch-1767191875055.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1767191875055.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1767789797202.md b/.changeset/bump-patch-1767789797202.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1767789797202.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1767922007435.md b/.changeset/bump-patch-1767922007435.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1767922007435.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1768188248094.md b/.changeset/bump-patch-1768188248094.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1768188248094.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/chatty-dingos-bathe.md b/.changeset/chatty-dingos-bathe.md new file mode 100644 index 0000000000000..e09c5e1c667e0 --- /dev/null +++ b/.changeset/chatty-dingos-bathe.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/apps': minor +'@rocket.chat/meteor': minor +--- + +Adds a new method to the Apps-Engine that allows apps to retrieve multiple rooms from database diff --git a/.changeset/chatty-lizards-reflect.md b/.changeset/chatty-lizards-reflect.md new file mode 100644 index 0000000000000..17f269cb70a20 --- /dev/null +++ b/.changeset/chatty-lizards-reflect.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/ddp-client": major +--- + +Removes the deprecated livechat:getTagsList method + +Removes the deprecated livechat:getUnitsFromUser method + +Removes the deprecated livechat:getFirstRoomMessage method + +Removes the deprecated livechat:getDepartmentForwardRestrictions method + diff --git a/.changeset/chatty-roses-help.md b/.changeset/chatty-roses-help.md new file mode 100644 index 0000000000000..3c4a5849c8daa --- /dev/null +++ b/.changeset/chatty-roses-help.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/i18n": patch +--- + +Adds invitation request support to rooms diff --git a/.changeset/chilly-cobras-look.md b/.changeset/chilly-cobras-look.md new file mode 100644 index 0000000000000..9e7bba192bba2 --- /dev/null +++ b/.changeset/chilly-cobras-look.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +--- + +Removes deprecated VoIP permissions diff --git a/.changeset/cold-chairs-taste.md b/.changeset/cold-chairs-taste.md new file mode 100644 index 0000000000000..e12a576569534 --- /dev/null +++ b/.changeset/cold-chairs-taste.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where its not being possible to change the password in account security page diff --git a/.changeset/cold-chefs-rhyme.md b/.changeset/cold-chefs-rhyme.md new file mode 100644 index 0000000000000..5dc1bbdc43c81 --- /dev/null +++ b/.changeset/cold-chefs-rhyme.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Disables read receipts indicators in federated rooms. This feature will be re-enabled when fully compatible with federation. diff --git a/.changeset/cool-turtles-bathe.md b/.changeset/cool-turtles-bathe.md new file mode 100644 index 0000000000000..ab3f8b80285c6 --- /dev/null +++ b/.changeset/cool-turtles-bathe.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': major +'@rocket.chat/media-calls': minor +'@rocket.chat/i18n': minor +--- + +Makes Voice Calls enabled by default when available diff --git a/.changeset/cuddly-eels-perform.md b/.changeset/cuddly-eels-perform.md new file mode 100644 index 0000000000000..bc99f3c96e302 --- /dev/null +++ b/.changeset/cuddly-eels-perform.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes members tab > add members not removing selected items diff --git a/.changeset/curly-bats-wink.md b/.changeset/curly-bats-wink.md new file mode 100644 index 0000000000000..79bd2e7e37918 --- /dev/null +++ b/.changeset/curly-bats-wink.md @@ -0,0 +1,16 @@ +--- +'@rocket.chat/model-typings': major +'@rocket.chat/core-typings': major +'@rocket.chat/rest-typings': major +'@rocket.chat/ui-contexts': major +'@rocket.chat/ui-voip': major +'@rocket.chat/models': major +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +'@rocket.chat/apps-engine': minor +'@rocket.chat/core-services': minor +'@rocket.chat/message-types': minor +'@rocket.chat/ddp-client': minor +--- + +Removes deprecated VoIP from Omnichannel diff --git a/.changeset/cyan-mayflies-juggle.md b/.changeset/cyan-mayflies-juggle.md new file mode 100644 index 0000000000000..593798b63ba1c --- /dev/null +++ b/.changeset/cyan-mayflies-juggle.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:saveUnit` method diff --git a/.changeset/dull-rabbits-add.md b/.changeset/dull-rabbits-add.md new file mode 100644 index 0000000000000..1cfd27a2c7701 --- /dev/null +++ b/.changeset/dull-rabbits-add.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `authorization:removeUserFromRole` method diff --git a/.changeset/dull-tips-look.md b/.changeset/dull-tips-look.md new file mode 100644 index 0000000000000..8867fc650dc12 --- /dev/null +++ b/.changeset/dull-tips-look.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +"@rocket.chat/federation-matrix": patch +--- + +Fixes an issue where cases of invites that were canceled or disinvited were not being handled. diff --git a/.changeset/empty-buses-walk.md b/.changeset/empty-buses-walk.md new file mode 100644 index 0000000000000..c24e5acce300e --- /dev/null +++ b/.changeset/empty-buses-walk.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/gazzodown": major +"@rocket.chat/i18n": major +"@rocket.chat/ui-client": major +--- + +Promotes Timestamp Parser from preview state to stable diff --git a/.changeset/fair-dryers-behave.md b/.changeset/fair-dryers-behave.md new file mode 100644 index 0000000000000..a9c57c42042e3 --- /dev/null +++ b/.changeset/fair-dryers-behave.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': major +'@rocket.chat/meteor': major +--- + +Removes ecdh functionality and related settings diff --git a/.changeset/fast-ligers-unite.md b/.changeset/fast-ligers-unite.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/fast-ligers-unite.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/fast-walls-eat.md b/.changeset/fast-walls-eat.md new file mode 100644 index 0000000000000..1c82cd4c32513 --- /dev/null +++ b/.changeset/fast-walls-eat.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes deprecated meteor method `unmuteUserInRoom` diff --git a/.changeset/few-masks-punch.md b/.changeset/few-masks-punch.md new file mode 100644 index 0000000000000..cf455b251bd8d --- /dev/null +++ b/.changeset/few-masks-punch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated `canAccessRoom` meteor method diff --git a/.changeset/fifty-rice-smash.md b/.changeset/fifty-rice-smash.md new file mode 100644 index 0000000000000..51ca36cdc84d4 --- /dev/null +++ b/.changeset/fifty-rice-smash.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated Realtime API method: `livechat:getAnalyticsChartData` diff --git a/.changeset/five-months-shake.md b/.changeset/five-months-shake.md new file mode 100644 index 0000000000000..b694672c84358 --- /dev/null +++ b/.changeset/five-months-shake.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:returnAsInquiry` method diff --git a/.changeset/forty-pears-divide.md b/.changeset/forty-pears-divide.md new file mode 100644 index 0000000000000..b7c9c57357aff --- /dev/null +++ b/.changeset/forty-pears-divide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes deprecated method `livechat:removeBusinessHour` diff --git a/.changeset/forty-spiders-sneeze.md b/.changeset/forty-spiders-sneeze.md new file mode 100644 index 0000000000000..f09f83231e92d --- /dev/null +++ b/.changeset/forty-spiders-sneeze.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `removeCustomField` method diff --git a/.changeset/four-timers-enjoy.md b/.changeset/four-timers-enjoy.md new file mode 100644 index 0000000000000..8ce7068fa7bb1 --- /dev/null +++ b/.changeset/four-timers-enjoy.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/model-typings': major +'@rocket.chat/core-typings': major +'@rocket.chat/rest-typings': major +'@rocket.chat/models': major +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +'@rocket.chat/core-services': minor +--- + +Removes Deprecated FreeSwitch integration diff --git a/.changeset/fresh-tables-raise.md b/.changeset/fresh-tables-raise.md new file mode 100644 index 0000000000000..89978df7f5421 --- /dev/null +++ b/.changeset/fresh-tables-raise.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes the deprecated `getUserRoles` method in favor of the `/v1/roles.getUsersInPublicRoles` endpoint. diff --git a/.changeset/fuzzy-plants-hammer.md b/.changeset/fuzzy-plants-hammer.md new file mode 100644 index 0000000000000..5b4f0a647f889 --- /dev/null +++ b/.changeset/fuzzy-plants-hammer.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Changes the position of the buttons in Unique ID change detected modal in order to highlight configuration update instead of new workspace diff --git a/.changeset/fuzzy-teachers-juggle.md b/.changeset/fuzzy-teachers-juggle.md new file mode 100644 index 0000000000000..fce7559bbcb6a --- /dev/null +++ b/.changeset/fuzzy-teachers-juggle.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/mock-providers': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue that could cause slashcommands to disappear for the user in certain high-availability scenarios diff --git a/.changeset/gentle-dingos-retire.md b/.changeset/gentle-dingos-retire.md new file mode 100644 index 0000000000000..526facc864e23 --- /dev/null +++ b/.changeset/gentle-dingos-retire.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `setAdminStatus` method diff --git a/.changeset/gold-keys-compare.md b/.changeset/gold-keys-compare.md new file mode 100644 index 0000000000000..452169781c04c --- /dev/null +++ b/.changeset/gold-keys-compare.md @@ -0,0 +1,4 @@ +--- +"@rocket.chat/meteor": major +--- +Removes support of MongoDB versions 5.x and 6.x diff --git a/.changeset/gold-zoos-sneeze.md b/.changeset/gold-zoos-sneeze.md new file mode 100644 index 0000000000000..a38ff5c5fb604 --- /dev/null +++ b/.changeset/gold-zoos-sneeze.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated meteor method `muteUserInRoom` diff --git a/.changeset/good-files-accept.md b/.changeset/good-files-accept.md new file mode 100644 index 0000000000000..ccc6ce33f5b25 --- /dev/null +++ b/.changeset/good-files-accept.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/rest-typings": major +--- + +Removes the deprecated method `createToken` diff --git a/.changeset/grumpy-colts-collect.md b/.changeset/grumpy-colts-collect.md new file mode 100644 index 0000000000000..9406052d0b132 --- /dev/null +++ b/.changeset/grumpy-colts-collect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where custom status is not updating immediately if the value is empty diff --git a/.changeset/grumpy-readers-give.md b/.changeset/grumpy-readers-give.md new file mode 100644 index 0000000000000..085e9e3d88cc0 --- /dev/null +++ b/.changeset/grumpy-readers-give.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': patch +'@rocket.chat/meteor': patch +--- + +Adds an execution flag to apps-engine runtime that helps prevent the publishing of faulty builds diff --git a/.changeset/happy-carpets-draw.md b/.changeset/happy-carpets-draw.md new file mode 100644 index 0000000000000..f59a1a06c7bbf --- /dev/null +++ b/.changeset/happy-carpets-draw.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': major +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +--- + +Promotes Resizable Contextualbars from preview state to stable. diff --git a/.changeset/happy-news-reflect.md b/.changeset/happy-news-reflect.md new file mode 100644 index 0000000000000..c69eb08eacdd1 --- /dev/null +++ b/.changeset/happy-news-reflect.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/fuselage-ui-kit": patch +--- + +Fixes an issue with the `action` block inside `Info Card` ui-kit element not dispatching actions. diff --git a/.changeset/honest-knives-cough.md b/.changeset/honest-knives-cough.md new file mode 100644 index 0000000000000..2329bec3a251a --- /dev/null +++ b/.changeset/honest-knives-cough.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `setReaction` meteor method diff --git a/.changeset/hungry-fans-wait.md b/.changeset/hungry-fans-wait.md new file mode 100644 index 0000000000000..7d8594f3470aa --- /dev/null +++ b/.changeset/hungry-fans-wait.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Validates attachment fields to require `title` and `value` properties on APIs `chat.postMessage` and `chat.sendMessage`. diff --git a/.changeset/large-jobs-smell.md b/.changeset/large-jobs-smell.md new file mode 100644 index 0000000000000..a04338844742b --- /dev/null +++ b/.changeset/large-jobs-smell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue with the build that prevented Deno runtime to run on air-gapped environments diff --git a/.changeset/large-planes-destroy.md b/.changeset/large-planes-destroy.md new file mode 100644 index 0000000000000..e2541e1df68f5 --- /dev/null +++ b/.changeset/large-planes-destroy.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes create channel modal not validating federated access permission diff --git a/.changeset/lazy-pianos-care.md b/.changeset/lazy-pianos-care.md new file mode 100644 index 0000000000000..d5afd4cb714af --- /dev/null +++ b/.changeset/lazy-pianos-care.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Enhance user's deactivated state handling to correctly distinguish between pending and deactivated users. diff --git a/.changeset/lemon-garlics-check.md b/.changeset/lemon-garlics-check.md new file mode 100644 index 0000000000000..6216aaf24dd9a --- /dev/null +++ b/.changeset/lemon-garlics-check.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes license add-on validations for federated rooms diff --git a/.changeset/long-swans-sin.md b/.changeset/long-swans-sin.md new file mode 100644 index 0000000000000..b5dd7a9c16b4c --- /dev/null +++ b/.changeset/long-swans-sin.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Changes the HTTP code of `/api/v1/method.call` and `/api/v1/method.callAnon` in case of internal errors diff --git a/.changeset/loud-elephants-happen.md b/.changeset/loud-elephants-happen.md new file mode 100644 index 0000000000000..0f505ff2445dd --- /dev/null +++ b/.changeset/loud-elephants-happen.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-signaling': patch +--- + +Fixes an issue where the client would play the Call Ended sound effect when the user started calling someone on a different session diff --git a/.changeset/many-walls-cheat.md b/.changeset/many-walls-cheat.md new file mode 100644 index 0000000000000..190539f09b1f2 --- /dev/null +++ b/.changeset/many-walls-cheat.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduces a new variation of the `Icon` element to `ui-kit` through the new `framed` optional property. diff --git a/.changeset/many-walls-impress.md b/.changeset/many-walls-impress.md new file mode 100644 index 0000000000000..058723b41d421 --- /dev/null +++ b/.changeset/many-walls-impress.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Adds deprecation warning on `livechat:addMonitor` with new endpoint replacing it; `livechat/monitors.create` diff --git a/.changeset/many-windows-perform.md b/.changeset/many-windows-perform.md new file mode 100644 index 0000000000000..d84dce497937c --- /dev/null +++ b/.changeset/many-windows-perform.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated Realtime API method: `livechat:closeRoom` diff --git a/.changeset/metal-moose-travel.md b/.changeset/metal-moose-travel.md new file mode 100644 index 0000000000000..71c2f66f5e02c --- /dev/null +++ b/.changeset/metal-moose-travel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a condition where the `SAML_Custom_Default_default_user_role` setting, used to define the default SAML role when none is provided, would fail when a role name was used instead of an ID. diff --git a/.changeset/metal-rocks-behave.md b/.changeset/metal-rocks-behave.md new file mode 100644 index 0000000000000..f6db936b32968 --- /dev/null +++ b/.changeset/metal-rocks-behave.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes deprecated method `saveUserProfile` diff --git a/.changeset/nasty-beans-breathe.md b/.changeset/nasty-beans-breathe.md new file mode 100644 index 0000000000000..8ba5361d40462 --- /dev/null +++ b/.changeset/nasty-beans-breathe.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `setCustomFields` method diff --git a/.changeset/nasty-moons-speak.md b/.changeset/nasty-moons-speak.md new file mode 100644 index 0000000000000..d709bd58c2021 --- /dev/null +++ b/.changeset/nasty-moons-speak.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds invitation badge to room members list diff --git a/.changeset/neat-spoons-cover.md b/.changeset/neat-spoons-cover.md new file mode 100644 index 0000000000000..ed5941e748a69 --- /dev/null +++ b/.changeset/neat-spoons-cover.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated Realtime API method: `livechat:removeAllClosedRooms` diff --git a/.changeset/nervous-doors-knock.md b/.changeset/nervous-doors-knock.md new file mode 100644 index 0000000000000..aa4084d7b8ded --- /dev/null +++ b/.changeset/nervous-doors-knock.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `getAvatarSuggestion` method diff --git a/.changeset/nervous-melons-cough.md b/.changeset/nervous-melons-cough.md new file mode 100644 index 0000000000000..870cdfacdff37 --- /dev/null +++ b/.changeset/nervous-melons-cough.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes the deprecated meteor method: `livechat:addMonitor` diff --git a/.changeset/nervous-wombats-look.md b/.changeset/nervous-wombats-look.md new file mode 100644 index 0000000000000..6a3baaea2f50c --- /dev/null +++ b/.changeset/nervous-wombats-look.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:saveDepartment` method diff --git a/.changeset/ninety-dodos-confess.md b/.changeset/ninety-dodos-confess.md new file mode 100644 index 0000000000000..b85badf29d0bf --- /dev/null +++ b/.changeset/ninety-dodos-confess.md @@ -0,0 +1,15 @@ +--- +'@rocket.chat/authorization-service': minor +'@rocket.chat/core-services': minor +'@rocket.chat/message-types': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/apps-engine': minor +'@rocket.chat/abac': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/jwt': minor +'@rocket.chat/meteor': minor +--- + +Adds Attribute Based Access Control (ABAC) for private channels & private teams. diff --git a/.changeset/ninety-hats-swim.md b/.changeset/ninety-hats-swim.md new file mode 100644 index 0000000000000..43ea75a82d383 --- /dev/null +++ b/.changeset/ninety-hats-swim.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:setUpConnection` method diff --git a/.changeset/odd-pigs-hang.md b/.changeset/odd-pigs-hang.md new file mode 100644 index 0000000000000..b3db5fdbdafaf --- /dev/null +++ b/.changeset/odd-pigs-hang.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where iframe external commands sent via `window.postMessage` were not being handled correctly when Rocket.Chat was embedded inside an iframe. diff --git a/.changeset/olive-pens-think.md b/.changeset/olive-pens-think.md new file mode 100644 index 0000000000000..994b14dad0833 --- /dev/null +++ b/.changeset/olive-pens-think.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incorrect URL generation in Global Search "Jump to message" feature, resolving navigation issues when jumping to messages across different channels. diff --git a/.changeset/orange-poets-marry.md b/.changeset/orange-poets-marry.md new file mode 100644 index 0000000000000..8da7abd288b76 --- /dev/null +++ b/.changeset/orange-poets-marry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `getRoomRoles` method diff --git a/.changeset/pink-months-compare.md b/.changeset/pink-months-compare.md new file mode 100644 index 0000000000000..27e345823957a --- /dev/null +++ b/.changeset/pink-months-compare.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `authorization:deleteRole` method diff --git a/.changeset/plenty-flowers-help.md b/.changeset/plenty-flowers-help.md new file mode 100644 index 0000000000000..65f178303d5cc --- /dev/null +++ b/.changeset/plenty-flowers-help.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:removeRoom` method diff --git a/.changeset/poor-apricots-heal.md b/.changeset/poor-apricots-heal.md new file mode 100644 index 0000000000000..420ed96068488 --- /dev/null +++ b/.changeset/poor-apricots-heal.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes push notifications continuing after logout due to missing token cleanup. diff --git a/.changeset/poor-trains-mate.md b/.changeset/poor-trains-mate.md new file mode 100644 index 0000000000000..90b7882bab726 --- /dev/null +++ b/.changeset/poor-trains-mate.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `appId` parameter from the `oauth-apps.get` endpoint. diff --git a/.changeset/popular-items-smash.md b/.changeset/popular-items-smash.md new file mode 100644 index 0000000000000..84316444911ba --- /dev/null +++ b/.changeset/popular-items-smash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `sendFileLivechatMessage` method diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..1363127f09ead --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,201 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "8.0.0-develop", + "rocketchat-services": "2.0.37", + "@rocket.chat/uikit-playground": "0.7.2", + "@rocket.chat/account-service": "0.4.46", + "@rocket.chat/authorization-service": "0.4.46", + "@rocket.chat/ddp-streamer": "0.3.46", + "@rocket.chat/omnichannel-transcript": "0.4.46", + "@rocket.chat/presence-service": "0.4.46", + "@rocket.chat/queue-worker": "0.4.46", + "@rocket.chat/abac": "0.0.1", + "@rocket.chat/federation-matrix": "0.0.8", + "@rocket.chat/license": "1.1.6", + "@rocket.chat/media-calls": "0.1.2", + "@rocket.chat/network-broker": "0.2.25", + "@rocket.chat/omni-core-ee": "0.0.11", + "@rocket.chat/omnichannel-services": "0.3.43", + "@rocket.chat/pdf-worker": "0.3.25", + "@rocket.chat/presence": "0.2.46", + "@rocket.chat/ui-theming": "0.4.4", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.46", + "@rocket.chat/apps": "0.5.25", + "@rocket.chat/apps-engine": "1.58.0", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.3", + "@rocket.chat/core-services": "0.11.2", + "@rocket.chat/core-typings": "8.0.0-develop", + "@rocket.chat/cron": "0.1.46", + "@rocket.chat/ddp-client": "0.3.46", + "@rocket.chat/desktop-api": "1.1.0", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.4", + "@rocket.chat/fuselage-ui-kit": "25.0.2", + "@rocket.chat/gazzodown": "25.0.2", + "@rocket.chat/http-router": "7.9.13", + "@rocket.chat/i18n": "1.13.0", + "@rocket.chat/instance-status": "0.1.46", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.23.17", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/media-signaling": "0.1.0", + "@rocket.chat/message-parser": "0.31.32", + "@rocket.chat/message-types": "0.0.1", + "@rocket.chat/mock-providers": "0.4.6", + "@rocket.chat/model-typings": "1.9.2", + "@rocket.chat/models": "1.8.2", + "@rocket.chat/mongo-adapter": "0.0.2", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/omni-core": "0.0.11", + "@rocket.chat/password-policies": "0.1.0", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.27", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "8.0.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.12", + "@rocket.chat/storybook-config": "0.0.2", + "@rocket.chat/tools": "0.2.3", + "@rocket.chat/tracing": "0.0.1", + "@rocket.chat/tsconfig": "0.0.0", + "@rocket.chat/ui-avatar": "21.0.2", + "@rocket.chat/ui-client": "25.0.2", + "@rocket.chat/ui-composer": "0.5.3", + "@rocket.chat/ui-contexts": "25.0.2", + "@rocket.chat/ui-kit": "0.38.0", + "@rocket.chat/ui-video-conf": "25.0.2", + "@rocket.chat/ui-voip": "15.0.2", + "@rocket.chat/web-ui-registration": "25.0.2" + }, + "changesets": [ + "afraid-parents-bake", + "beige-seahorses-reply", + "blue-beans-check", + "breezy-timers-flow", + "brown-carrots-bathe", + "brown-llamas-worry", + "bump-patch-1766456337926", + "bump-patch-1767191875055", + "bump-patch-1767789797202", + "bump-patch-1767922007435", + "bump-patch-1768188248094", + "chatty-dingos-bathe", + "chatty-lizards-reflect", + "chatty-roses-help", + "chilly-cobras-look", + "cold-chairs-taste", + "cold-chefs-rhyme", + "cool-turtles-bathe", + "cuddly-eels-perform", + "curly-bats-wink", + "cyan-mayflies-juggle", + "dull-rabbits-add", + "dull-tips-look", + "empty-buses-walk", + "fair-dryers-behave", + "fast-ligers-unite", + "fast-walls-eat", + "few-masks-punch", + "fifty-rice-smash", + "five-months-shake", + "forty-pears-divide", + "forty-spiders-sneeze", + "four-timers-enjoy", + "fresh-tables-raise", + "fuzzy-plants-hammer", + "fuzzy-teachers-juggle", + "gentle-dingos-retire", + "gold-keys-compare", + "gold-zoos-sneeze", + "good-files-accept", + "grumpy-colts-collect", + "grumpy-readers-give", + "happy-carpets-draw", + "happy-news-reflect", + "honest-knives-cough", + "hungry-fans-wait", + "large-jobs-smell", + "large-planes-destroy", + "lazy-pianos-care", + "lemon-garlics-check", + "long-swans-sin", + "loud-elephants-happen", + "many-walls-cheat", + "many-walls-impress", + "many-windows-perform", + "metal-moose-travel", + "metal-rocks-behave", + "nasty-beans-breathe", + "nasty-moons-speak", + "neat-spoons-cover", + "nervous-doors-knock", + "nervous-melons-cough", + "nervous-wombats-look", + "ninety-dodos-confess", + "ninety-hats-swim", + "odd-pigs-hang", + "olive-pens-think", + "orange-poets-marry", + "pink-months-compare", + "plenty-flowers-help", + "poor-apricots-heal", + "poor-trains-mate", + "popular-items-smash", + "proud-dingos-sell", + "purple-mayflies-approve", + "quick-turtles-count", + "quiet-bees-turn", + "real-grapes-itch", + "rich-dogs-wonder", + "rotten-bees-behave", + "rotten-pugs-trade", + "selfish-countries-sleep", + "seven-badgers-dress", + "seven-otters-turn", + "shaggy-otters-think", + "sharp-beers-search", + "short-pots-lay", + "six-squids-pretend", + "sixty-bikes-know", + "slimy-ads-sing", + "slimy-hairs-wink", + "smooth-birds-crash", + "spicy-nails-design", + "stale-shoes-serve", + "strange-spiders-act", + "strong-bags-train", + "strong-bats-swim", + "strong-maps-act", + "swift-ears-sparkle", + "ten-carrots-melt", + "tender-wolves-promise", + "thick-badgers-grab", + "thick-guests-compete", + "thick-wasps-turn", + "tidy-laws-wink", + "tough-baboons-wash", + "tricky-trees-tan", + "twelve-feet-repeat", + "twelve-forks-destroy", + "twelve-years-act", + "twenty-cars-decide", + "two-pets-knock", + "two-turtles-try", + "violet-cats-lick", + "weak-frogs-relax", + "wet-papayas-buy", + "wet-walls-drive", + "wicked-yaks-join", + "wild-hairs-carry" + ] +} diff --git a/.changeset/proud-dingos-sell.md b/.changeset/proud-dingos-sell.md new file mode 100644 index 0000000000000..17d7910431550 --- /dev/null +++ b/.changeset/proud-dingos-sell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes the setting `API_Use_REST_For_DDP_Calls`. Turning this on meant websocket was only used for realtime data/events, and any other meteor method calls goes over method.call endpoint. For microservice deployments, this had to be turned on. Now method calls will always happen over http endpoints. diff --git a/.changeset/purple-mayflies-approve.md b/.changeset/purple-mayflies-approve.md new file mode 100644 index 0000000000000..997daff3dce0a --- /dev/null +++ b/.changeset/purple-mayflies-approve.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': major +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +--- + +Promotes quick reactions from preview state to stable diff --git a/.changeset/quick-turtles-count.md b/.changeset/quick-turtles-count.md new file mode 100644 index 0000000000000..028ea55d86eb6 --- /dev/null +++ b/.changeset/quick-turtles-count.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/rest-typings": major +--- + +Removes `/api/v1/banners.getnew` deprecated endpoint diff --git a/.changeset/quiet-bees-turn.md b/.changeset/quiet-bees-turn.md new file mode 100644 index 0000000000000..e2312ccb478af --- /dev/null +++ b/.changeset/quiet-bees-turn.md @@ -0,0 +1,10 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/ddp-client": major +"@rocket.chat/livechat": major +"@rocket.chat/rest-typings": major +--- + +Removes the `livechat:transfer` deprecated method +Removes the `livechat/room.transfer` deprecated endpoint +Creates the `livechat/visitor.department.transfer` for visitors department transfer use diff --git a/.changeset/real-grapes-itch.md b/.changeset/real-grapes-itch.md new file mode 100644 index 0000000000000..e4eda2815fa45 --- /dev/null +++ b/.changeset/real-grapes-itch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where it‘s not being possible to configure department's `chatClosingTags` without enabling `requestTagBeforeClosingTag` diff --git a/.changeset/rich-dogs-wonder.md b/.changeset/rich-dogs-wonder.md new file mode 100644 index 0000000000000..2eeb95649f61d --- /dev/null +++ b/.changeset/rich-dogs-wonder.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue that caused Third-party login to not work properly diff --git a/.changeset/rotten-bees-behave.md b/.changeset/rotten-bees-behave.md new file mode 100644 index 0000000000000..1a14da3c2cbf0 --- /dev/null +++ b/.changeset/rotten-bees-behave.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +removes the deprecated meteor method: `livechat:removeMonitor` diff --git a/.changeset/rotten-pugs-trade.md b/.changeset/rotten-pugs-trade.md new file mode 100644 index 0000000000000..37515336702d6 --- /dev/null +++ b/.changeset/rotten-pugs-trade.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/tools": patch +--- + +Adds improvements to the push notifications logic; the logic now truncates messages and titles larger than 240, and 65 characters respectively. diff --git a/.changeset/selfish-countries-sleep.md b/.changeset/selfish-countries-sleep.md new file mode 100644 index 0000000000000..63227b9df6271 --- /dev/null +++ b/.changeset/selfish-countries-sleep.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated Realtime API method: `livechat:getRoutingConfig` diff --git a/.changeset/seven-badgers-dress.md b/.changeset/seven-badgers-dress.md new file mode 100644 index 0000000000000..3d2b1ef6ad85b --- /dev/null +++ b/.changeset/seven-badgers-dress.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes deprecated Realtime API method: `livechat:changeLivechatStatus` diff --git a/.changeset/seven-otters-turn.md b/.changeset/seven-otters-turn.md new file mode 100644 index 0000000000000..f67e3d7fe6016 --- /dev/null +++ b/.changeset/seven-otters-turn.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Adds deprecation warning for `livechat:saveTag` and new endpoint to replace it; `livechat/tags.save` diff --git a/.changeset/shaggy-otters-think.md b/.changeset/shaggy-otters-think.md new file mode 100644 index 0000000000000..961fea652556b --- /dev/null +++ b/.changeset/shaggy-otters-think.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes the deprecated param `hideRoomsWithNoActivity` and adjust the api tests accordingly. Endpoint always returns rooms that are not empty. diff --git a/.changeset/sharp-beers-search.md b/.changeset/sharp-beers-search.md new file mode 100644 index 0000000000000..0b674b3318eec --- /dev/null +++ b/.changeset/sharp-beers-search.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `insertOrUpdateUser` meteor method diff --git a/.changeset/short-pots-lay.md b/.changeset/short-pots-lay.md new file mode 100644 index 0000000000000..aa14ad39ed4eb --- /dev/null +++ b/.changeset/short-pots-lay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:removeTag` method diff --git a/.changeset/six-squids-pretend.md b/.changeset/six-squids-pretend.md new file mode 100644 index 0000000000000..f150528ffb545 --- /dev/null +++ b/.changeset/six-squids-pretend.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Fixes role assignment precedence in SAML provisioning, ensuring that SAML-specific default roles take priority over global registration roles, and preventing role merging when both are configured. diff --git a/.changeset/sixty-bikes-know.md b/.changeset/sixty-bikes-know.md new file mode 100644 index 0000000000000..dd74fbbc1ff0c --- /dev/null +++ b/.changeset/sixty-bikes-know.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the client failed to load properly when the “First Channel After Login” setting began with a hash (#), ensuring users are routed to the correct channel. diff --git a/.changeset/slimy-ads-sing.md b/.changeset/slimy-ads-sing.md new file mode 100644 index 0000000000000..cad18a974e9c4 --- /dev/null +++ b/.changeset/slimy-ads-sing.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes /v1/users.logout not marking user sessions as logged out, leaving stale sessions active. diff --git a/.changeset/slimy-hairs-wink.md b/.changeset/slimy-hairs-wink.md new file mode 100644 index 0000000000000..46357b7cf4418 --- /dev/null +++ b/.changeset/slimy-hairs-wink.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated setUsername method + diff --git a/.changeset/smooth-birds-crash.md b/.changeset/smooth-birds-crash.md new file mode 100644 index 0000000000000..6dfe28b72b16c --- /dev/null +++ b/.changeset/smooth-birds-crash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `removeCannedResponse` method diff --git a/.changeset/spicy-nails-design.md b/.changeset/spicy-nails-design.md new file mode 100644 index 0000000000000..23a5f82e7bb5e --- /dev/null +++ b/.changeset/spicy-nails-design.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +"@rocket.chat/ddp-streamer": patch +"@rocket.chat/presence": patch +--- + +Ensures presence stays accurate by refreshing connections on heartbeats and removing stale sessions. \ No newline at end of file diff --git a/.changeset/stale-shoes-serve.md b/.changeset/stale-shoes-serve.md new file mode 100644 index 0000000000000..6f1b6fea42ee2 --- /dev/null +++ b/.changeset/stale-shoes-serve.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/account-service': patch +--- + +Fixes an issue where some DDP streamer requests were returning before processing was completed diff --git a/.changeset/strange-spiders-act.md b/.changeset/strange-spiders-act.md new file mode 100644 index 0000000000000..5cbdafc2f2ab9 --- /dev/null +++ b/.changeset/strange-spiders-act.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `saveCannedResponse` method diff --git a/.changeset/strong-bags-train.md b/.changeset/strong-bags-train.md new file mode 100644 index 0000000000000..3f53beead4969 --- /dev/null +++ b/.changeset/strong-bags-train.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/federation-matrix": patch +--- + +Fixes an issue where membership updates were not reflected when the user was the first member on their own server. diff --git a/.changeset/strong-bats-swim.md b/.changeset/strong-bats-swim.md new file mode 100644 index 0000000000000..d0287981e1e08 --- /dev/null +++ b/.changeset/strong-bats-swim.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:sendTranscript` method diff --git a/.changeset/strong-maps-act.md b/.changeset/strong-maps-act.md new file mode 100644 index 0000000000000..04c8c2c6c64ae --- /dev/null +++ b/.changeset/strong-maps-act.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/ui-voip": minor +--- + +Introduces an info button to voice call's in-chat history message, which opens a contextual bar with more detailed information about the voice call. diff --git a/.changeset/swift-ears-sparkle.md b/.changeset/swift-ears-sparkle.md new file mode 100644 index 0000000000000..d332456dba3af --- /dev/null +++ b/.changeset/swift-ears-sparkle.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/rest-typings": major +--- + +Removes the deprecated `/api/v1/rooms.upload` endpoint diff --git a/.changeset/ten-carrots-melt.md b/.changeset/ten-carrots-melt.md new file mode 100644 index 0000000000000..7dcb452a2f92c --- /dev/null +++ b/.changeset/ten-carrots-melt.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated livechat:resumeOnHold method + diff --git a/.changeset/tender-wolves-promise.md b/.changeset/tender-wolves-promise.md new file mode 100644 index 0000000000000..234a01290239b --- /dev/null +++ b/.changeset/tender-wolves-promise.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing dispatch of `startup` iframe event on client startup. diff --git a/.changeset/thick-badgers-grab.md b/.changeset/thick-badgers-grab.md new file mode 100644 index 0000000000000..5cfa0235a6871 --- /dev/null +++ b/.changeset/thick-badgers-grab.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes `addUserToRole` and `removeUserFromRole` type declaration and deprecation logger diff --git a/.changeset/thick-guests-compete.md b/.changeset/thick-guests-compete.md new file mode 100644 index 0000000000000..20b7fb8ca0405 --- /dev/null +++ b/.changeset/thick-guests-compete.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:saveAgentInfo` method diff --git a/.changeset/thick-wasps-turn.md b/.changeset/thick-wasps-turn.md new file mode 100644 index 0000000000000..ea892a87f5556 --- /dev/null +++ b/.changeset/thick-wasps-turn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes inconsistency in roomLeft event payload by aligning it to the standard outgoing events signature. diff --git a/.changeset/tidy-laws-wink.md b/.changeset/tidy-laws-wink.md new file mode 100644 index 0000000000000..6a745931a3855 --- /dev/null +++ b/.changeset/tidy-laws-wink.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/rest-typings": major +--- + +Removes the deprecated `GET` Method from `/api/v1/apps` diff --git a/.changeset/tough-baboons-wash.md b/.changeset/tough-baboons-wash.md new file mode 100644 index 0000000000000..b8655b4514868 --- /dev/null +++ b/.changeset/tough-baboons-wash.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/rest-typings': major +'@rocket.chat/ddp-client': major +'@rocket.chat/logger': major +'@rocket.chat/meteor': major +--- + +Removes stdout logging functionality, related components and settings diff --git a/.changeset/tricky-trees-tan.md b/.changeset/tricky-trees-tan.md new file mode 100644 index 0000000000000..f1cb631d720b6 --- /dev/null +++ b/.changeset/tricky-trees-tan.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Removes sensitive data from outgoing events debug logs. diff --git a/.changeset/twelve-feet-repeat.md b/.changeset/twelve-feet-repeat.md new file mode 100644 index 0000000000000..577096ec2b015 --- /dev/null +++ b/.changeset/twelve-feet-repeat.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated 'e2e.updateGroupKey' method and related type declarations diff --git a/.changeset/twelve-forks-destroy.md b/.changeset/twelve-forks-destroy.md new file mode 100644 index 0000000000000..b4cc7321bed9f --- /dev/null +++ b/.changeset/twelve-forks-destroy.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Adds invitation badge to sidebar diff --git a/.changeset/twelve-years-act.md b/.changeset/twelve-years-act.md new file mode 100644 index 0000000000000..d118252c3fffe --- /dev/null +++ b/.changeset/twelve-years-act.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes deprecated method `livechat:saveBusinessHour` diff --git a/.changeset/twenty-cars-decide.md b/.changeset/twenty-cars-decide.md new file mode 100644 index 0000000000000..f7ac52c68a517 --- /dev/null +++ b/.changeset/twenty-cars-decide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:saveCustomField` method diff --git a/.changeset/two-pets-knock.md b/.changeset/two-pets-knock.md new file mode 100644 index 0000000000000..c568f32c9a6ea --- /dev/null +++ b/.changeset/two-pets-knock.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/rest-typings": major +--- + +Removes the deprecated roleName parameter from /api/v1/roles.addUserToRole and /api/v1/roles.removeUserFromRole + +Removes the ability to pass a role name to the role parameter type from /api/v1/roles.getUsersInRole diff --git a/.changeset/two-turtles-try.md b/.changeset/two-turtles-try.md new file mode 100644 index 0000000000000..1d8816caad368 --- /dev/null +++ b/.changeset/two-turtles-try.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": major +"@rocket.chat/ddp-client": major +--- + +Removes the deprecated `setUserPassword` method diff --git a/.changeset/violet-cats-lick.md b/.changeset/violet-cats-lick.md new file mode 100644 index 0000000000000..5e3a68ccaa36a --- /dev/null +++ b/.changeset/violet-cats-lick.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-signaling': patch +--- + +Adds a timeout to ensure transferred calls stop ringing if there's no response from the final user diff --git a/.changeset/weak-frogs-relax.md b/.changeset/weak-frogs-relax.md new file mode 100644 index 0000000000000..4a19730ec1462 --- /dev/null +++ b/.changeset/weak-frogs-relax.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Adds deprecation warning for `livechat:removeBusinessHour` and new endpoint to replace it; `livechat/business-hours.remove` diff --git a/.changeset/wet-papayas-buy.md b/.changeset/wet-papayas-buy.md new file mode 100644 index 0000000000000..1995c1acd95e2 --- /dev/null +++ b/.changeset/wet-papayas-buy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-calls': patch +--- + +Fixes improper handling of errors when SIP integration is configured incorrectly diff --git a/.changeset/wet-walls-drive.md b/.changeset/wet-walls-drive.md new file mode 100644 index 0000000000000..b8297000fa903 --- /dev/null +++ b/.changeset/wet-walls-drive.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-client': major +'@rocket.chat/meteor': major +--- + +Promotes Enhanced Navigation from preview state to stable. diff --git a/.changeset/wicked-yaks-join.md b/.changeset/wicked-yaks-join.md new file mode 100644 index 0000000000000..92946e56e2a8e --- /dev/null +++ b/.changeset/wicked-yaks-join.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deprecated `livechat:takeInquiry` method diff --git a/.changeset/wild-hairs-carry.md b/.changeset/wild-hairs-carry.md new file mode 100644 index 0000000000000..9a7c69766ee80 --- /dev/null +++ b/.changeset/wild-hairs-carry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes the deprecated `authorization:addUserToRole` method diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b98cde5f50299..699185914302b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,6 @@ /apps/meteor/tests/unit/server @RocketChat/backend /apps/meteor/app/apps/ @RocketChat/apps /apps/meteor/app/livechat @RocketChat/omnichannel -/apps/meteor/app/voip @RocketChat/omnichannel /apps/meteor/app/sms @RocketChat/omnichannel /apps/meteor/app/api @RocketChat/backend /apps/meteor/app/federation @RocketChat/backend @@ -27,12 +26,9 @@ /packages/models @RocketChat/Architecture apps/meteor/server/startup/migrations @RocketChat/Architecture /apps/meteor/packages/rocketchat-livechat @RocketChat/omnichannel -/apps/meteor/server/services/voip-asterisk @RocketChat/omnichannel -/apps/meteor/server/services/omnichannel-voip @RocketChat/omnichannel /apps/meteor/server/features/EmailInbox @RocketChat/omnichannel /apps/meteor/ee/app/canned-responses @RocketChat/omnichannel /apps/meteor/ee/app/livechat @RocketChat/omnichannel /apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel /apps/meteor/client/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/omnichannel @RocketChat/omnichannel -/apps/meteor/client/components/voip @RocketChat/omnichannel diff --git a/.github/actions/docker-image-size-tracker/action.yml b/.github/actions/docker-image-size-tracker/action.yml index 6c43c3844aa90..ea132c8a99c49 100644 --- a/.github/actions/docker-image-size-tracker/action.yml +++ b/.github/actions/docker-image-size-tracker/action.yml @@ -6,6 +6,9 @@ inputs: github-token: description: 'GitHub token for commenting on PRs' required: true + ci-pat: + description: 'GitHub token for committing to history branch' + required: true registry: description: 'Container registry (e.g., ghcr.io)' required: false @@ -258,6 +261,9 @@ runs: cd /tmp/history-worktree git add "history/${timestamp}.json" git commit -m "Add measurement for ${timestamp} (${commit_sha:0:7})" + git config --global user.email "ci@rocket.chat" + git config --global user.name "rocketchat-ci[bot]" + git config --global url.https://${{ inputs.ci-pat }}@github.com/.insteadOf https://github.com/ git push origin image-size-history cd - diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index 934e51bc2ffc4..13fe7da5b302a 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -34,7 +34,7 @@ runs: id: cache-build with: path: /tmp/Rocket.Chat.tar.gz - key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.type }}-rc-build-${{ inputs.source-hash }} + key: ${{ runner.arch }}-${{ runner.os }}-${{ inputs.type }}-rc-build-${{ inputs.source-hash }} - name: Set Swap Space uses: pierotofy/set-swap-space@master @@ -73,27 +73,27 @@ runs: if: steps.cache-build.outputs.cache-hit != 'true' with: path: ./node_modules/.vite - key: vite-local-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json') }} + key: vite-local-cache-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('package.json') }} restore-keys: | - vite-local-cache-${{ runner.os }}-${{ runner.arch }}- + vite-local-cache-${{ runner.arch }}-${{ runner.os }}- - name: Cache meteor local uses: actions/cache@v3 if: steps.cache-build.outputs.cache-hit != 'true' with: path: ./apps/meteor/.meteor/local - key: meteor-local-cache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.type }}-${{ hashFiles('apps/meteor/.meteor/versions') }} + key: meteor-local-cache-${{ runner.arch }}-${{ runner.os }}-${{ inputs.type }}-${{ hashFiles('apps/meteor/.meteor/versions') }} restore-keys: | - meteor-local-cache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.type }}- + meteor-local-cache-${{ runner.arch }}-${{ runner.os }}-${{ inputs.type }}- - name: Cache meteor uses: actions/cache@v3 if: steps.cache-build.outputs.cache-hit != 'true' with: path: ~/.meteor - key: meteor-cache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.type }}-${{ hashFiles('apps/meteor/.meteor/release') }} + key: meteor-cache-${{ runner.arch }}-${{ runner.os }}-${{ inputs.type }}-${{ hashFiles('apps/meteor/.meteor/release') }} restore-keys: | - meteor-cache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.type }}- + meteor-cache-${{ runner.arch }}-${{ runner.os }}-${{ inputs.type }}- - name: Install Meteor shell: bash diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index faa3f6d7ed17d..16eb1317327e5 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -69,6 +69,14 @@ runs: with: deno-version: ${{ inputs.deno-version }} + - name: Configure mongodb-memory-server cache + shell: bash + run: | + CACHE_DIR="${GITHUB_WORKSPACE}/node_modules/.cache/mongodb-memory-server" + mkdir -p "$CACHE_DIR" + echo "MONGOMS_DOWNLOAD_DIR=$CACHE_DIR" >> $GITHUB_ENV + echo "MONGOMS_PREFER_GLOBAL_PATH=false" >> $GITHUB_ENV + - name: yarn login shell: bash if: inputs.NPM_TOKEN @@ -90,3 +98,8 @@ runs: if: inputs.install && inputs.type == 'production' shell: bash run: YARN_ENABLE_HARDENED_MODE=${{ inputs.HARDENED_MODE }} yarn workspaces focus --all --production + + - name: restore yarn config file + if: inputs.install + shell: bash + run: git checkout -- .yarnrc.yml diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yml index 4231e139bddb1..1d71e06e2c473 100644 --- a/.github/actions/setup-playwright/action.yml +++ b/.github/actions/setup-playwright/action.yml @@ -11,7 +11,7 @@ runs: path: | ~/.cache/ms-playwright # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. - key: playwright-${{ runner.os }}-${{ runner.arch }}-1.52.0 + key: playwright-${{ runner.arch }}-${{ runner.os }}-1.52.0 - name: Install Playwright shell: bash diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index 8f686758cf82a..6fb692c311a1d 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -208,13 +208,13 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index c6368e5574db3..52e3b7c085d9d 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -30,7 +30,7 @@ jobs: with: swap-size-gb: 4 - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -44,7 +44,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: packages-build path: /tmp @@ -55,7 +55,7 @@ jobs: tar -xzf /tmp/RocketChat-packages-build.tar.gz -C . - name: Cache TypeCheck - uses: actions/cache@v4 + uses: actions/cache@v5 if: matrix.check == 'ts' with: path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo @@ -91,7 +91,7 @@ jobs: run: yarn turbo run typecheck - name: Cache eslint - uses: actions/cache@v4 + uses: actions/cache@v5 if: matrix.check == 'lint' with: path: ./apps/meteor/.eslintcache diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index 2494a819c1197..b8d92d6413158 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -11,7 +11,7 @@ jobs: deploy-preview: runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: rharkor/caching-for-turbo@v1.8 - name: Setup NodeJS diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 828a50aaa4040..1b6a73a04944a 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -44,10 +44,6 @@ on: coverage: required: false type: string - db-watcher-disabled: - default: 'true' - required: false - type: string secrets: CR_USER: required: true @@ -88,7 +84,7 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'false' && ' [legacy watchers]' || '' }}${{ inputs.coverage == matrix.mongodb-version && ' coverage' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) + name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.coverage == matrix.mongodb-version && ' coverage' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) steps: - name: Collect Workflow Telemetry @@ -115,7 +111,7 @@ jobs: username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -129,7 +125,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: packages-build path: /tmp @@ -178,7 +174,6 @@ jobs: env: ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} - DISABLE_DB_WATCHERS: ${{ inputs.db-watcher-disabled }} run: | DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait @@ -253,13 +248,13 @@ jobs: if: inputs.type == 'ui' && always() uses: actions/upload-artifact@v5 with: - name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} + name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }}${{ inputs.db-watcher-disabled == 'true' && '-no-watcher' || '' }} path: ./apps/meteor/tests/e2e/.playwright* include-hidden-files: true - name: Show server logs if E2E test failed if: failure() - run: docker compose -f docker-compose-ci.yml logs rocketchat authorization-service queue-worker-service ddp-streamer-service account-service presence-service stream-hub-service omnichannel-transcript-service + run: docker compose -f docker-compose-ci.yml logs rocketchat authorization-service queue-worker-service ddp-streamer-service account-service presence-service omnichannel-transcript-service - name: Show mongo logs if E2E test failed if: failure() diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 6729088ba107e..0606607121db3 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -24,7 +24,7 @@ jobs: name: Test Storybook steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -38,7 +38,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: packages-build path: /tmp @@ -55,7 +55,7 @@ jobs: env: STORYBOOK_DISABLE_TELEMETRY: 1 - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: flags: unit verbose: true diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 6f1d0096e0be6..5d35f7ef31bef 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -34,7 +34,7 @@ jobs: theme: dark job_summary: true comment_on_pr: false - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -48,7 +48,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: packages-build path: /tmp @@ -61,7 +61,7 @@ jobs: - name: Unit Test run: yarn testunit - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: flags: unit verbose: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c077b50b820f..5f0d8829ff24f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat $GITHUB_EVENT_PATH - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # with: # sparse-checkout: | # package.json @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-24.04-arm needs: [release-versions] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: sparse-checkout: | package.json @@ -158,7 +158,7 @@ jobs: fi; curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5\", \"6\", \"7\", \"8\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\",\"compatibleMongoVersions\": [\"8.2\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update packages-build: @@ -167,12 +167,12 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Cache build - uses: actions/cache@v4 + uses: actions/cache@v5 id: packages-cache-build with: path: | /tmp/RocketChat-packages-build.tar.gz - key: ${{ runner.OS }}-packages-build-${{ needs.release-versions.outputs.source-hash }} + key: ${{ runner.arch }}-${{ runner.os }}-packages-build-${{ needs.release-versions.outputs.source-hash }} - name: Debug cache-hit run: echo "cache-hit=${{ steps.packages-cache-build.outputs.cache-hit }}" @@ -183,7 +183,7 @@ jobs: with: swap-size-gb: 4 - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 if: steps.packages-cache-build.outputs.cache-hit != 'true' - name: Setup NodeJS @@ -198,13 +198,13 @@ jobs: HARDENED_MODE: '1' - name: Cache vite - uses: actions/cache@v4 + uses: actions/cache@v5 if: steps.packages-cache-build.outputs.cache-hit != 'true' with: path: ./node_modules/.vite - key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} + key: vite-local-cache-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('package.json') }} restore-keys: | - vite-local-cache-${{ runner.os }}- + vite-local-cache-${{ runner.arch }}-${{ runner.os }}- - uses: rharkor/caching-for-turbo@v1.8 if: steps.packages-cache-build.outputs.cache-hit != 'true' @@ -257,7 +257,7 @@ jobs: job_summary: true comment_on_pr: false - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: ./.github/actions/meteor-build with: @@ -282,7 +282,7 @@ jobs: service: [ [authorization-service, queue-worker-service, ddp-streamer-service], - [account-service, presence-service, stream-hub-service, omnichannel-transcript-service], + [account-service, presence-service, omnichannel-transcript-service], [rocketchat], ] type: @@ -298,10 +298,10 @@ jobs: type: coverage steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Restore packages build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: packages-build path: /tmp @@ -378,7 +378,7 @@ jobs: LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') with: sparse-checkout: | @@ -396,7 +396,7 @@ jobs: - name: Download manifests if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: manifests-* path: /tmp/manifests @@ -448,12 +448,13 @@ jobs: contents: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Track Docker image sizes uses: ./.github/actions/docker-image-size-tracker with: github-token: ${{ secrets.GITHUB_TOKEN }} + ci-pat: ${{ secrets.CI_PAT }} registry: ghcr.io repository: ${{ needs.release-versions.outputs.lowercase-repo }} tag: ${{ needs.release-versions.outputs.gh-docker-tag }} @@ -542,7 +543,7 @@ jobs: release: ee transporter: 'nats://nats:4222' enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} - mongodb-version: "['5.0', '8.2']" + mongodb-version: "['8.2']" coverage: '8.2' node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} @@ -556,33 +557,6 @@ jobs: name: 🔨 Test UI (EE) needs: [checks, build-gh-docker-publish, release-versions] - uses: ./.github/workflows/ci-test-e2e.yml - with: - type: ui - release: ee - transporter: 'nats://nats:4222' - enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} - shard: '[1, 2, 3, 4, 5]' - total-shard: 5 - mongodb-version: "['5.0']" - node-version: ${{ needs.release-versions.outputs.node-version }} - deno-version: ${{ needs.release-versions.outputs.deno-version }} - lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} - gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} - retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} - secrets: - CR_USER: ${{ secrets.CR_USER }} - CR_PAT: ${{ secrets.CR_PAT }} - QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} - REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} - REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} - - test-ui-ee-watcher: - name: 🔨 Test UI (EE) - needs: [checks, build-gh-docker-publish, release-versions] - uses: ./.github/workflows/ci-test-e2e.yml with: type: ui @@ -598,7 +572,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} - db-watcher-disabled: 'false' secrets: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} @@ -607,14 +580,13 @@ jobs: REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} - test-federation-matrix: name: 🔨 Test Federation Matrix needs: [checks, build-gh-docker-publish, packages-build, release-versions] runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -627,7 +599,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore turbo build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 continue-on-error: true with: name: turbo-build @@ -653,25 +625,26 @@ jobs: working-directory: ./ee/packages/federation-matrix env: ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }} - ENTERPRISE_LICENSE_RC1: ${{ secrets.ENTERPRISE_LICENSE_RC1 }} + ENTERPRISE_LICENSE_RC1: ZAikY+LLaal7mT6RNYxpyWEmMQyucrl50/7pYBXqHczc90j+RLwF+T0xuCT2pIpKMC5DxcZ1TtkV6MYJk5whrwmap+mQ0FV+VpILJlL0i4T21K4vMfzZXTWm/pzcAy2fMTUNH+mUA9HTBD6lYYh40KnbGXPAd80VbZk0MO/WbWBm2dOT0YCwfvlRyurRqkDAQrftLaffzCNUsMKk0fh+MKs73UDHZQDp1yvs7WoGpPu5ZVi5mTBOt3ZKVz5KjGfClLwJptFPmW1w6nKelAiJBDPpjcX1ylfjxpnBoixko7uN52zlyaeoAYwfRcdDLnZ8k0Ou6tui/vTQUXjGIjHw2AhMaKwonn4E9LYpuA1KEXt08qJL5J3ZtjSCV1T+A9Z3zFhhLgp5dxP/PPUbxDn/P8XKp7nXM9duIfcCMlnea7V8ixEyCHwwvKQaXVVidcsUGtB8CwS0GlsAEBLOzqMehuQUK2rdQ4WgEz3AYveikeVvSzgBHvyXsxssWAThc0Mht0eEJqdDhUB2QeZ2WmPsaSSD639Z4WgjSUoR0zh8bfqepH+2XRcUryXe2yN+iU+3POzi9wfg0k65MxXT8pBg3PD5RHnR8oflEP0tpZts33JiBhYRxX3MKplAFm4dMuphTsDJTh+e534pT7IPuZF79QSVaLEWZfVVVb7nGFtmMwA= QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }} + PR_NUMBER: ${{ github.event.number }} run: yarn test:integration --image "${ROCKETCHAT_IMAGE}" report-coverage: name: 📊 Report Coverage runs-on: ubuntu-24.04 - needs: [release-versions, test-api-ee, test-ui-ee, test-ui-ee-watcher] + needs: [release-versions, test-api-ee, test-ui-ee] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: ${{ needs.release-versions.outputs.node-version }} - name: Restore coverage folder - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: coverage-* path: /tmp/coverage @@ -692,7 +665,7 @@ jobs: include-hidden-files: true - name: Report API coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: files: /tmp/coverage_report/api/lcov.info working-directory: . @@ -701,7 +674,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Report UI coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: files: /tmp/coverage_report/ui/lcov.info working-directory: . @@ -712,7 +685,7 @@ jobs: tests-done: name: ✅ Tests Done runs-on: ubuntu-24.04-arm - needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-ui-ee-watcher, test-federation-matrix] + needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-federation-matrix] if: always() steps: - name: Test finish aggregation @@ -741,10 +714,6 @@ jobs: exit 1 fi - if [[ '${{ needs.test-ui-ee-watcher.result }}' != 'success' ]]; then - exit 1 - fi - if [[ '${{ needs.test-federation-matrix.result }}' != 'success' ]]; then exit 1 fi @@ -758,7 +727,7 @@ jobs: needs: [build-gh-docker-publish, release-versions] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: sparse-checkout: | package.json @@ -766,7 +735,7 @@ jobs: ref: ${{ github.ref }} - name: Restore build - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: build-production path: /tmp/build @@ -815,7 +784,7 @@ jobs: LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: sparse-checkout: | docker-compose-ci.yml @@ -837,7 +806,7 @@ jobs: - name: Download manifests if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: manifests-* path: /tmp/manifests @@ -928,7 +897,7 @@ jobs: - docker-image-publish - release-versions steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: sparse-checkout: | package.json @@ -961,7 +930,7 @@ jobs: fi; curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5\", \"6\", \"7\", \"8\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"8.2\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update # Makes build fail if the release isn't there diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6ab011a49e0d8..3fdfebd584f75 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 9df9526ff1aa6..49266b3dd5042 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ github.event.inputs.base-ref }} fetch-depth: 0 diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index fdb7c40f93963..eb1ecbf6b9497 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -13,7 +13,7 @@ jobs: if: startsWith(github.head_ref, 'release-') steps: - name: Checkout Repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.CI_PAT }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4dbc8d10d32fd..0465375a1727e 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout Repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.CI_PAT }} diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 6c1b556809a39..ab9af0347da01 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -1,12 +1,12 @@ name: Release candidate cut on: schedule: - - cron: '28 17 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH + - cron: '28 17 21 * *' # run at minute 28 to avoid the chance of delay due to high load on GH jobs: new-release: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ github.ref_name }} fetch-depth: 0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 89fb59d0c5845..552b8b5b6917d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: permissions: issues: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v10 with: days-before-issue-stale: 14 days-before-issue-close: 14 diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 5eb69ad3effbb..00e1ee8125ad6 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 22.16.0 diff --git a/.yarnrc.yml b/.yarnrc.yml index 128ce3e5464c8..6543b343613d7 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -10,16 +10,6 @@ nodeLinker: node-modules plugins: - path: .yarn/plugins/@yarnpkg/plugin-engines.cjs - spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js" - -supportedArchitectures: - cpu: - - arm64 - - x64 - libc: - - glibc - - musl - os: - - linux + spec: 'https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js' yarnPath: .yarn/releases/yarn-4.11.0.cjs diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore deleted file mode 100644 index f7051767d7727..0000000000000 --- a/apps/meteor/.eslintignore +++ /dev/null @@ -1,16 +0,0 @@ -/node_modules/ -#/tests/e2e/ -/packages/ -/app/emoji-emojione/generateEmojiIndex.js -/public/ -/private/moment-locales/ -/imports/ -/ee/server/services/dist/ -!/.mocharc.js -!/.mocharc.*.js -!/.scripts/ -!/.storybook/ -!/client/.eslintrc.js -!/ee/client/.eslintrc.js -/storybook-static/ -/packages/ diff --git a/apps/meteor/.eslintrc.json b/apps/meteor/.eslintrc.json index 41b02ad3a4520..84af970d6b52f 100644 --- a/apps/meteor/.eslintrc.json +++ b/apps/meteor/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:you-dont-need-lodash-underscore/compatible"], + "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:you-dont-need-lodash-underscore/compatible", "plugin:storybook/recommended"], "globals": { "__meteor_bootstrap__": false, "__meteor_runtime_config__": false, @@ -22,6 +22,21 @@ } ] }, + "ignorePatterns": [ + "app/emoji-emojione/generateEmojiIndex.js", + "public", + "private/moment-locales", + "imports", + "ee/server/services/dist", + "!.mocharc.js", + "!.mocharc.*.js", + "!.scripts", + "!.storybook", + "!client/.eslintrc.js", + "!ee/client/.eslintrc.js", + "storybook-static", + "packages" + ], "overrides": [ { "files": ["**/*.ts", "**/*.tsx"], diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 0dce43f784ee4..4242b98f6bed3 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -3,7 +3,6 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -rocketchat:ddp rocketchat:mongo-config rocketchat:livechat rocketchat:streamer diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 04d79cb550212..04fcb4d920bdd 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -70,7 +70,6 @@ reactive-dict@1.3.2 reactive-var@1.0.13 reload@1.3.2 retry@1.1.1 -rocketchat:ddp@0.0.1 rocketchat:livechat@0.0.1 rocketchat:mongo-config@0.0.1 rocketchat:streamer@1.1.0 diff --git a/apps/meteor/.storybook/main.ts b/apps/meteor/.storybook/main.ts index 4e358f72d3b52..1cceaff3ff602 100644 --- a/apps/meteor/.storybook/main.ts +++ b/apps/meteor/.storybook/main.ts @@ -9,7 +9,7 @@ export default { addons: [ getAbsolutePath('@storybook/addon-essentials'), getAbsolutePath('@storybook/addon-interactions'), - getAbsolutePath('@storybook/addon-webpack5-compiler-babel'), + getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), getAbsolutePath('@storybook/addon-styling-webpack'), getAbsolutePath('@storybook/addon-a11y'), ], @@ -28,6 +28,7 @@ export default { 'react$': require.resolve('../../../node_modules/react'), // 'react/jsx-runtime': require.resolve('../../../node_modules/react/jsx-runtime'), '@tanstack/react-query': require.resolve('../../../node_modules/@tanstack/react-query'), + '@rocket.chat/fuselage$': require.resolve('../../../node_modules/@rocket.chat/fuselage'), 'swiper/swiper.css$': 'swiper/css', 'swiper/modules/navigation/navigation.min.css$': 'swiper/css/navigation', 'swiper/modules/keyboard/keyboard.min.css$': 'swiper/css/keyboard', @@ -45,7 +46,6 @@ export default { config.plugins?.push( new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.js')), new webpack.NormalModuleReplacementPlugin(/(app)\/*.*\/(server)\/*/, require.resolve('./mocks/empty.ts')), - new webpack.NormalModuleReplacementPlugin(/^sip.js/, require.resolve('./mocks/empty.ts')), ); return config; diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index df458e70638a2..a91e61835c003 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,486 @@ # @rocket.chat/meteor +## 8.0.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.0.0-rc.5 + - @rocket.chat/rest-typings@8.0.0-rc.5 + - @rocket.chat/abac@0.1.0-rc.5 + - @rocket.chat/federation-matrix@0.0.9-rc.5 + - @rocket.chat/license@1.1.7-rc.5 + - @rocket.chat/media-calls@0.2.0-rc.5 + - @rocket.chat/omnichannel-services@0.3.44-rc.5 + - @rocket.chat/pdf-worker@0.3.26-rc.5 + - @rocket.chat/presence@0.2.47-rc.5 + - @rocket.chat/api-client@0.2.47-rc.5 + - @rocket.chat/apps@0.6.0-rc.5 + - @rocket.chat/core-services@0.12.0-rc.5 + - @rocket.chat/cron@0.1.47-rc.5 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.5 + - @rocket.chat/gazzodown@26.0.0-rc.5 + - @rocket.chat/http-router@7.9.14-rc.5 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.5 + - @rocket.chat/ui-avatar@22.0.0-rc.5 + - @rocket.chat/ui-client@26.0.0-rc.5 + - @rocket.chat/ui-contexts@26.0.0-rc.5 + - @rocket.chat/ui-voip@16.0.0-rc.5 + - @rocket.chat/web-ui-registration@26.0.0-rc.5 + - @rocket.chat/models@2.0.0-rc.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.26-rc.5 + - @rocket.chat/omni-core-ee@0.0.12-rc.5 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/ui-video-conf@26.0.0-rc.5 + - @rocket.chat/instance-status@0.1.47-rc.5 + - @rocket.chat/omni-core@0.0.12-rc.5 +
+ +## 8.0.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.0.0-rc.4 + - @rocket.chat/rest-typings@8.0.0-rc.4 + - @rocket.chat/abac@0.1.0-rc.4 + - @rocket.chat/federation-matrix@0.0.9-rc.4 + - @rocket.chat/license@1.1.7-rc.4 + - @rocket.chat/media-calls@0.2.0-rc.4 + - @rocket.chat/omnichannel-services@0.3.44-rc.4 + - @rocket.chat/pdf-worker@0.3.26-rc.4 + - @rocket.chat/presence@0.2.47-rc.4 + - @rocket.chat/api-client@0.2.47-rc.4 + - @rocket.chat/apps@0.6.0-rc.4 + - @rocket.chat/core-services@0.12.0-rc.4 + - @rocket.chat/cron@0.1.47-rc.4 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.4 + - @rocket.chat/gazzodown@26.0.0-rc.4 + - @rocket.chat/http-router@7.9.14-rc.4 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.4 + - @rocket.chat/ui-avatar@22.0.0-rc.4 + - @rocket.chat/ui-client@26.0.0-rc.4 + - @rocket.chat/ui-contexts@26.0.0-rc.4 + - @rocket.chat/ui-voip@16.0.0-rc.4 + - @rocket.chat/web-ui-registration@26.0.0-rc.4 + - @rocket.chat/models@2.0.0-rc.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.26-rc.4 + - @rocket.chat/omni-core-ee@0.0.12-rc.4 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/ui-video-conf@26.0.0-rc.4 + - @rocket.chat/instance-status@0.1.47-rc.4 + - @rocket.chat/omni-core@0.0.12-rc.4 +
+ +## 8.0.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies [ae1e2faaeb6f7a086f8affde4c8a81e55e2a0e04]: + + - @rocket.chat/media-signaling@0.1.1-rc.1 + - @rocket.chat/media-calls@0.2.0-rc.3 + - @rocket.chat/core-services@0.12.0-rc.3 + - @rocket.chat/ui-voip@16.0.0-rc.3 + - @rocket.chat/abac@0.1.0-rc.3 + - @rocket.chat/federation-matrix@0.0.9-rc.3 + - @rocket.chat/network-broker@0.2.26-rc.3 + - @rocket.chat/omni-core-ee@0.0.12-rc.3 + - @rocket.chat/omnichannel-services@0.3.44-rc.3 + - @rocket.chat/presence@0.2.47-rc.3 + - @rocket.chat/ui-contexts@26.0.0-rc.3 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.3 + - @rocket.chat/gazzodown@26.0.0-rc.3 + - @rocket.chat/ui-avatar@22.0.0-rc.3 + - @rocket.chat/ui-client@26.0.0-rc.3 + - @rocket.chat/ui-video-conf@26.0.0-rc.3 + - @rocket.chat/web-ui-registration@26.0.0-rc.3 + - @rocket.chat/core-typings@8.0.0-rc.3 + - @rocket.chat/rest-typings@8.0.0-rc.3 + - @rocket.chat/license@1.1.7-rc.3 + - @rocket.chat/pdf-worker@0.3.26-rc.3 + - @rocket.chat/api-client@0.2.47-rc.3 + - @rocket.chat/apps@0.6.0-rc.3 + - @rocket.chat/cron@0.1.47-rc.3 + - @rocket.chat/http-router@7.9.14-rc.3 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.3 + - @rocket.chat/models@2.0.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/instance-status@0.1.47-rc.3 + - @rocket.chat/omni-core@0.0.12-rc.3 +
+ +## 8.0.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#38007](https://github.com/RocketChat/Rocket.Chat/pull/38007)) Changes the HTTP code of `/api/v1/method.call` and `/api/v1/method.callAnon` in case of internal errors + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.0.0-rc.2 + - @rocket.chat/rest-typings@8.0.0-rc.2 + - @rocket.chat/abac@0.1.0-rc.2 + - @rocket.chat/federation-matrix@0.0.9-rc.2 + - @rocket.chat/license@1.1.7-rc.2 + - @rocket.chat/media-calls@0.2.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.44-rc.2 + - @rocket.chat/pdf-worker@0.3.26-rc.2 + - @rocket.chat/presence@0.2.47-rc.2 + - @rocket.chat/api-client@0.2.47-rc.2 + - @rocket.chat/apps@0.6.0-rc.2 + - @rocket.chat/core-services@0.12.0-rc.2 + - @rocket.chat/cron@0.1.47-rc.2 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.2 + - @rocket.chat/gazzodown@26.0.0-rc.2 + - @rocket.chat/http-router@7.9.14-rc.2 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.2 + - @rocket.chat/ui-avatar@22.0.0-rc.2 + - @rocket.chat/ui-client@26.0.0-rc.2 + - @rocket.chat/ui-contexts@26.0.0-rc.2 + - @rocket.chat/ui-voip@16.0.0-rc.2 + - @rocket.chat/web-ui-registration@26.0.0-rc.2 + - @rocket.chat/models@2.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.26-rc.2 + - @rocket.chat/omni-core-ee@0.0.12-rc.2 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/ui-video-conf@26.0.0-rc.2 + - @rocket.chat/instance-status@0.1.47-rc.2 + - @rocket.chat/omni-core@0.0.12-rc.2 +
+ +## 8.0.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.0.0-rc.1 + - @rocket.chat/rest-typings@8.0.0-rc.1 + - @rocket.chat/abac@0.1.0-rc.1 + - @rocket.chat/federation-matrix@0.0.9-rc.1 + - @rocket.chat/license@1.1.7-rc.1 + - @rocket.chat/media-calls@0.2.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.44-rc.1 + - @rocket.chat/pdf-worker@0.3.26-rc.1 + - @rocket.chat/presence@0.2.47-rc.1 + - @rocket.chat/api-client@0.2.47-rc.1 + - @rocket.chat/apps@0.6.0-rc.1 + - @rocket.chat/core-services@0.12.0-rc.1 + - @rocket.chat/cron@0.1.47-rc.1 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.1 + - @rocket.chat/gazzodown@26.0.0-rc.1 + - @rocket.chat/http-router@7.9.14-rc.1 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.1 + - @rocket.chat/ui-avatar@22.0.0-rc.1 + - @rocket.chat/ui-client@26.0.0-rc.1 + - @rocket.chat/ui-contexts@26.0.0-rc.1 + - @rocket.chat/ui-voip@16.0.0-rc.1 + - @rocket.chat/web-ui-registration@26.0.0-rc.1 + - @rocket.chat/models@2.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.26-rc.1 + - @rocket.chat/omni-core-ee@0.0.12-rc.1 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/ui-video-conf@26.0.0-rc.1 + - @rocket.chat/instance-status@0.1.47-rc.1 + - @rocket.chat/omni-core@0.0.12-rc.1 +
+ +## 8.0.0-rc.0 + +### Major Changes + +- ([#36829](https://github.com/RocketChat/Rocket.Chat/pull/36829)) Removes the deprecated sendConfirmationEmail method + +- ([#37460](https://github.com/RocketChat/Rocket.Chat/pull/37460)) Removes deprecated `livechat:removeUnit` method + +- ([#36836](https://github.com/RocketChat/Rocket.Chat/pull/36836)) Removes the deprecated livechat:getTagsList method + + Removes the deprecated livechat:getUnitsFromUser method + + Removes the deprecated livechat:getFirstRoomMessage method + + Removes the deprecated livechat:getDepartmentForwardRestrictions method + +- ([#37672](https://github.com/RocketChat/Rocket.Chat/pull/37672)) Removes deprecated VoIP permissions + +- ([#36941](https://github.com/RocketChat/Rocket.Chat/pull/36941)) Makes Voice Calls enabled by default when available + +- ([#37672](https://github.com/RocketChat/Rocket.Chat/pull/37672)) Removes deprecated VoIP from Omnichannel + +- ([#37461](https://github.com/RocketChat/Rocket.Chat/pull/37461)) Removes deprecated `livechat:saveUnit` method + +- ([#36864](https://github.com/RocketChat/Rocket.Chat/pull/36864)) Removes the deprecated `authorization:removeUserFromRole` method + +- ([#36976](https://github.com/RocketChat/Rocket.Chat/pull/36976)) Promotes Timestamp Parser from preview state to stable + +- ([#37123](https://github.com/RocketChat/Rocket.Chat/pull/37123)) Removes ecdh functionality and related settings + +- ([#35436](https://github.com/RocketChat/Rocket.Chat/pull/35436) by [@blackmamba1231](https://github.com/blackmamba1231)) Removes deprecated `canAccessRoom` meteor method + +- ([#36925](https://github.com/RocketChat/Rocket.Chat/pull/36925)) Removes deprecated Realtime API method: `livechat:getAnalyticsChartData` + +- ([#37392](https://github.com/RocketChat/Rocket.Chat/pull/37392)) Removes deprecated `livechat:returnAsInquiry` method + +- ([#37390](https://github.com/RocketChat/Rocket.Chat/pull/37390)) Removes deprecated `removeCustomField` method + +- ([#37672](https://github.com/RocketChat/Rocket.Chat/pull/37672)) Removes Deprecated FreeSwitch integration + +- ([#36851](https://github.com/RocketChat/Rocket.Chat/pull/36851)) Removes the deprecated `getUserRoles` method in favor of the `/v1/roles.getUsersInPublicRoles` endpoint. + +- ([#37388](https://github.com/RocketChat/Rocket.Chat/pull/37388)) Removes deprecated `setAdminStatus` method + +- ([#35961](https://github.com/RocketChat/Rocket.Chat/pull/35961)) Removes support of MongoDB versions 5.x and 6.x + +- ([#36823](https://github.com/RocketChat/Rocket.Chat/pull/36823)) Removes deprecated meteor method `muteUserInRoom` + +- ([#36825](https://github.com/RocketChat/Rocket.Chat/pull/36825)) Removes the deprecated method `createToken` + +- ([#37022](https://github.com/RocketChat/Rocket.Chat/pull/37022)) Promotes Resizable Contextualbars from preview state to stable. + +- ([#36837](https://github.com/RocketChat/Rocket.Chat/pull/36837)) Removes the deprecated `setReaction` meteor method + +- ([#36824](https://github.com/RocketChat/Rocket.Chat/pull/36824)) Removes deprecated Realtime API method: `livechat:closeRoom` + +- ([#36832](https://github.com/RocketChat/Rocket.Chat/pull/36832)) Removes the deprecated `setCustomFields` method + +- ([#36931](https://github.com/RocketChat/Rocket.Chat/pull/36931)) Removes deprecated Realtime API method: `livechat:removeAllClosedRooms` + +- ([#36830](https://github.com/RocketChat/Rocket.Chat/pull/36830)) Removes the deprecated `getAvatarSuggestion` method + +- ([#37462](https://github.com/RocketChat/Rocket.Chat/pull/37462)) Removes deprecated `livechat:saveDepartment` method + +- ([#37397](https://github.com/RocketChat/Rocket.Chat/pull/37397)) Removes deprecated `livechat:setUpConnection` method + +- Removes the deprecated `getRoomRoles` method + +- Removes the deprecated `authorization:deleteRole` method + +- ([#37391](https://github.com/RocketChat/Rocket.Chat/pull/37391)) Removes deprecated `livechat:removeRoom` method + +- ([#36849](https://github.com/RocketChat/Rocket.Chat/pull/36849)) Removes deprecated `appId` parameter from the `oauth-apps.get` endpoint. + +- ([#37463](https://github.com/RocketChat/Rocket.Chat/pull/37463)) Removes deprecated `sendFileLivechatMessage` method + +- ([#32590](https://github.com/RocketChat/Rocket.Chat/pull/32590)) Removes the setting `API_Use_REST_For_DDP_Calls`. Turning this on meant websocket was only used for realtime data/events, and any other meteor method calls goes over method.call endpoint. For microservice deployments, this had to be turned on. Now method calls will always happen over http endpoints. + +- ([#36966](https://github.com/RocketChat/Rocket.Chat/pull/36966)) Promotes quick reactions from preview state to stable + +- ([#36821](https://github.com/RocketChat/Rocket.Chat/pull/36821)) Removes `/api/v1/banners.getnew` deprecated endpoint + +- ([#36871](https://github.com/RocketChat/Rocket.Chat/pull/36871)) Removes the `livechat:transfer` deprecated method + Removes the `livechat/room.transfer` deprecated endpoint + Creates the `livechat/visitor.department.transfer` for visitors department transfer use +- ([#36924](https://github.com/RocketChat/Rocket.Chat/pull/36924)) Removes deprecated Realtime API method: `livechat:getRoutingConfig` + +- ([#36809](https://github.com/RocketChat/Rocket.Chat/pull/36809)) Removes deprecated Realtime API method: `livechat:changeLivechatStatus` + +- ([#36951](https://github.com/RocketChat/Rocket.Chat/pull/36951)) Removes the deprecated param `hideRoomsWithNoActivity` and adjust the api tests accordingly. Endpoint always returns rooms that are not empty. + +- ([#36838](https://github.com/RocketChat/Rocket.Chat/pull/36838)) Removes the deprecated `insertOrUpdateUser` meteor method + +- ([#37406](https://github.com/RocketChat/Rocket.Chat/pull/37406)) Removes deprecated `livechat:removeTag` method + +- ([#37810](https://github.com/RocketChat/Rocket.Chat/pull/37810)) Fixes role assignment precedence in SAML provisioning, ensuring that SAML-specific default roles take priority over global registration roles, and preventing role merging when both are configured. + +- ([#36831](https://github.com/RocketChat/Rocket.Chat/pull/36831)) Removes the deprecated setUsername method + +- ([#37446](https://github.com/RocketChat/Rocket.Chat/pull/37446)) Removes deprecated `removeCannedResponse` method + +- ([#37405](https://github.com/RocketChat/Rocket.Chat/pull/37405)) Removes deprecated `saveCannedResponse` method + +- ([#37396](https://github.com/RocketChat/Rocket.Chat/pull/37396)) Removes deprecated `livechat:sendTranscript` method + +- ([#36857](https://github.com/RocketChat/Rocket.Chat/pull/36857)) Removes the deprecated `/api/v1/rooms.upload` endpoint + +- ([#36908](https://github.com/RocketChat/Rocket.Chat/pull/36908)) Removes the deprecated livechat:resumeOnHold method + +- ([#36935](https://github.com/RocketChat/Rocket.Chat/pull/36935)) Removes `addUserToRole` and `removeUserFromRole` type declaration and deprecation logger + +- ([#37393](https://github.com/RocketChat/Rocket.Chat/pull/37393)) Removes deprecated `livechat:saveAgentInfo` method + +- ([#36907](https://github.com/RocketChat/Rocket.Chat/pull/36907)) Removes the deprecated `GET` Method from `/api/v1/apps` + +- ([#37114](https://github.com/RocketChat/Rocket.Chat/pull/37114)) Removes stdout logging functionality, related components and settings + +- ([#36647](https://github.com/RocketChat/Rocket.Chat/pull/36647)) Removes deprecated 'e2e.updateGroupKey' method and related type declarations + +- ([#37421](https://github.com/RocketChat/Rocket.Chat/pull/37421)) Removes deprecated `livechat:saveCustomField` method + +- ([#36896](https://github.com/RocketChat/Rocket.Chat/pull/36896)) Removes the deprecated roleName parameter from /api/v1/roles.addUserToRole and /api/v1/roles.removeUserFromRole + + Removes the ability to pass a role name to the role parameter type from /api/v1/roles.getUsersInRole + +- ([#36828](https://github.com/RocketChat/Rocket.Chat/pull/36828)) Removes the deprecated `setUserPassword` method + +- ([#37285](https://github.com/RocketChat/Rocket.Chat/pull/37285)) Promotes Enhanced Navigation from preview state to stable. + +- ([#37464](https://github.com/RocketChat/Rocket.Chat/pull/37464)) Removes deprecated `livechat:takeInquiry` method + +- ([#36865](https://github.com/RocketChat/Rocket.Chat/pull/36865)) Removes the deprecated `authorization:addUserToRole` method + +### Minor Changes + +- ([#36570](https://github.com/RocketChat/Rocket.Chat/pull/36570)) REST endpoint `/v1/users.createToken` is not deprecated anymore. It now requires a `secret` parameter to generate a token for a user. This change is part of the effort to enhance security by ensuring that tokens are generated with an additional layer of validation. The `secret` parameter is validated against a new environment variable `CREATE_TOKENS_FOR_USERS_SECRET`. + +- ([#37719](https://github.com/RocketChat/Rocket.Chat/pull/37719)) Adds a new method to the Apps-Engine that allows apps to retrieve multiple rooms from database + +- ([#37659](https://github.com/RocketChat/Rocket.Chat/pull/37659)) Changes the position of the buttons in Unique ID change detected modal in order to highlight configuration update instead of new workspace + +- ([#37233](https://github.com/RocketChat/Rocket.Chat/pull/37233)) Validates attachment fields to require `title` and `value` properties on APIs `chat.postMessage` and `chat.sendMessage`. + +- ([#37224](https://github.com/RocketChat/Rocket.Chat/pull/37224)) Enhance user's deactivated state handling to correctly distinguish between pending and deactivated users. + +- ([#37091](https://github.com/RocketChat/Rocket.Chat/pull/37091)) Adds Attribute Based Access Control (ABAC) for private channels & private teams. + +- ([#37771](https://github.com/RocketChat/Rocket.Chat/pull/37771)) Introduces an info button to voice call's in-chat history message, which opens a contextual bar with more detailed information about the voice call. + +### Patch Changes + +- ([#37663](https://github.com/RocketChat/Rocket.Chat/pull/37663)) Removes the deprecated meteor method: `livechat:saveTag` + +- ([#37688](https://github.com/RocketChat/Rocket.Chat/pull/37688)) Adds deprecation warning for `livechat:removeMonitor` and new endpoint replacing it; `livechat/monitor.remove` + +- ([#37690](https://github.com/RocketChat/Rocket.Chat/pull/37690)) Adds a deprecation warning for `livechat:saveBusinessHour` and new endpoint replacing it; `livechat/business-hours.save` + +- ([#37612](https://github.com/RocketChat/Rocket.Chat/pull/37612)) Adds invitation request support to rooms + +- ([#37745](https://github.com/RocketChat/Rocket.Chat/pull/37745)) Fixes an issue where its not being possible to change the password in account security page + +- ([#37721](https://github.com/RocketChat/Rocket.Chat/pull/37721)) Disables read receipts indicators in federated rooms. This feature will be re-enabled when fully compatible with federation. + +- ([#37823](https://github.com/RocketChat/Rocket.Chat/pull/37823)) Fixes members tab > add members not removing selected items + +- ([#37791](https://github.com/RocketChat/Rocket.Chat/pull/37791)) Fixes an issue where cases of invites that were canceled or disinvited were not being handled. + +- ([#37874](https://github.com/RocketChat/Rocket.Chat/pull/37874)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#36822](https://github.com/RocketChat/Rocket.Chat/pull/36822)) Removes deprecated meteor method `unmuteUserInRoom` + +- ([#37819](https://github.com/RocketChat/Rocket.Chat/pull/37819)) Removes deprecated method `livechat:removeBusinessHour` + +- ([#37654](https://github.com/RocketChat/Rocket.Chat/pull/37654)) Fixes an issue that could cause slashcommands to disappear for the user in certain high-availability scenarios + +- ([#37443](https://github.com/RocketChat/Rocket.Chat/pull/37443)) Fixes an issue where custom status is not updating immediately if the value is empty + +- ([#37730](https://github.com/RocketChat/Rocket.Chat/pull/37730)) Adds an execution flag to apps-engine runtime that helps prevent the publishing of faulty builds + +- ([#37722](https://github.com/RocketChat/Rocket.Chat/pull/37722)) Fixes an issue with the build that prevented Deno runtime to run on air-gapped environments + +- ([#37504](https://github.com/RocketChat/Rocket.Chat/pull/37504)) Fixes create channel modal not validating federated access permission + +- ([#37523](https://github.com/RocketChat/Rocket.Chat/pull/37523)) Fixes license add-on validations for federated rooms + +- ([#37061](https://github.com/RocketChat/Rocket.Chat/pull/37061)) Adds deprecation warning on `livechat:addMonitor` with new endpoint replacing it; `livechat/monitors.create` + +- ([#37713](https://github.com/RocketChat/Rocket.Chat/pull/37713)) Fixes a condition where the `SAML_Custom_Default_default_user_role` setting, used to define the default SAML role when none is provided, would fail when a role name was used instead of an ID. + +- ([#36827](https://github.com/RocketChat/Rocket.Chat/pull/36827)) Removes deprecated method `saveUserProfile` + +- ([#37643](https://github.com/RocketChat/Rocket.Chat/pull/37643)) Adds invitation badge to room members list + +- ([#37664](https://github.com/RocketChat/Rocket.Chat/pull/37664)) Removes the deprecated meteor method: `livechat:addMonitor` + +- ([#37829](https://github.com/RocketChat/Rocket.Chat/pull/37829)) Fixes an issue where iframe external commands sent via `window.postMessage` were not being handled correctly when Rocket.Chat was embedded inside an iframe. + +- ([#37717](https://github.com/RocketChat/Rocket.Chat/pull/37717)) Fixes incorrect URL generation in Global Search "Jump to message" feature, resolving navigation issues when jumping to messages across different channels. + +- ([#37845](https://github.com/RocketChat/Rocket.Chat/pull/37845)) Fixes push notifications continuing after logout due to missing token cleanup. + +- ([#37822](https://github.com/RocketChat/Rocket.Chat/pull/37822)) Fixes an issue where it‘s not being possible to configure department's `chatClosingTags` without enabling `requestTagBeforeClosingTag` + +- ([#37707](https://github.com/RocketChat/Rocket.Chat/pull/37707)) Fixes an issue that caused Third-party login to not work properly + +- ([#37662](https://github.com/RocketChat/Rocket.Chat/pull/37662)) removes the deprecated meteor method: `livechat:removeMonitor` + +- ([#37852](https://github.com/RocketChat/Rocket.Chat/pull/37852)) Adds improvements to the push notifications logic; the logic now truncates messages and titles larger than 240, and 65 characters respectively. + +- ([#37281](https://github.com/RocketChat/Rocket.Chat/pull/37281)) Adds deprecation warning for `livechat:saveTag` and new endpoint to replace it; `livechat/tags.save` + +- ([#37656](https://github.com/RocketChat/Rocket.Chat/pull/37656)) Fixes an issue where the client failed to load properly when the “First Channel After Login” setting began with a hash (#), ensuring users are routed to the correct channel. + +- ([#37846](https://github.com/RocketChat/Rocket.Chat/pull/37846)) Fixes /v1/users.logout not marking user sessions as logged out, leaving stale sessions active. + +- ([#37551](https://github.com/RocketChat/Rocket.Chat/pull/37551)) Ensures presence stays accurate by refreshing connections on heartbeats and removing stale sessions. + +- ([#37677](https://github.com/RocketChat/Rocket.Chat/pull/37677)) Fixes an issue where membership updates were not reflected when the user was the first member on their own server. + +- ([#37718](https://github.com/RocketChat/Rocket.Chat/pull/37718)) Fixes the missing dispatch of `startup` iframe event on client startup. + +- ([#37729](https://github.com/RocketChat/Rocket.Chat/pull/37729)) Fixes inconsistency in roomLeft event payload by aligning it to the standard outgoing events signature. + +- ([#37729](https://github.com/RocketChat/Rocket.Chat/pull/37729)) Removes sensitive data from outgoing events debug logs. + +- ([#37635](https://github.com/RocketChat/Rocket.Chat/pull/37635)) Adds invitation badge to sidebar + +- ([#37772](https://github.com/RocketChat/Rocket.Chat/pull/37772)) Removes deprecated method `livechat:saveBusinessHour` + +- ([#37775](https://github.com/RocketChat/Rocket.Chat/pull/37775)) Adds deprecation warning for `livechat:removeBusinessHour` and new endpoint to replace it; `livechat/business-hours.remove` + +-
Updated dependencies [347b8f973440f3e2239f79c00c2d9b430859eef2, 04d24848bd8733caefc45d42c53f004177865a53, b802430fbfdc7fa69a976468dc6dee6f3c4de26f, 872da49986436d2efa65fc42e416b45d706fd59c, 0ccb9692b434bc88b4bc5009d39e024c03f01b86, 176d5eae3fb249d7d20c3e260d9fadc1a56a2fca, ac11ea05ffadeca978c794ff38d5199d9acb2c29, 2de4547580c472f4458568629d7bf98fd5faf342, ac11ea05ffadeca978c794ff38d5199d9acb2c29, 1baa03cced8f94584da1224ad59cad86f219707a, 0c0258604632342f42fc36cabac2d6cfe0e477c4, f3f0b273ab49e9d1048ba43d52eb36005274905e, ac11ea05ffadeca978c794ff38d5199d9acb2c29, a1d65f493fdb039d34ef4f65d243a97931763f4b, 70872896b912004dc016297b6e875d873d81dc3a, bd5edfc2993c93bd77f42dcd30d38b57eeb50481, 94b87d9ef40647d77fe83f3f84dca46a94515b39, ddc935727e9a7275813006d9dcaa7fe866610844, 733c94b996204151f580de2dd7f3402124b70977, d3538e7045c41f91b8c561d44e5485ff93b93745, a5a7343a835b04812c70699be1b13e54f0e10d48, 73d9eb2783176954f42aa2cbeda8abf1d49ac260, 611e4cdfa04849416a58071646b853b95e9b817b, 476a070b0099b95e4c463ee85960c4dcfbd87120, 239f4b1171bcf448cfba345cc90c4b5cd7c21afc, 9e03ed5c5ea829c62c2da2de9413a27a4696f8a3, dc67590d14d510b069dee074c55314c56f74bb11, be80b724a636877294b5e5baa501d070941131dd, 4aa3634186d97f4144c39f6b42a65107d3d30df0, 4793aca8796d8a3b4c645a2ed685028067119d8d, 0b660a5933b137ae142d78318d8c4022f1f4f1ca, dccdcc5b4a0da4814f72a020bc4eccb8ea2497d8, 5ac1863be4c6e82666989f4b569928c0805691ff, ec0f8b435dd12c218adffa8892737c7ced4debb8, f056c451c2926e849f52b95fed957945398ef5f6, cb3c5e3455606a045f95f168dae6ed32a387697c, 5b3f93c47a03b628d613a77005e92021cd6cee4b, 8bf0bab1eb84f903976b7833691d17236eac8dcd, 55dc368f3f679e93bffb9f04efe3944832cf3336]: + + - @rocket.chat/rest-typings@8.0.0-rc.0 + - @rocket.chat/web-ui-registration@26.0.0-rc.0 + - @rocket.chat/apps-engine@1.59.0-rc.0 + - @rocket.chat/apps@0.6.0-rc.0 + - @rocket.chat/core-typings@8.0.0-rc.0 + - @rocket.chat/i18n@2.0.0-rc.0 + - @rocket.chat/media-calls@0.2.0-rc.0 + - @rocket.chat/model-typings@2.0.0-rc.0 + - @rocket.chat/ui-contexts@26.0.0-rc.0 + - @rocket.chat/ui-voip@16.0.0-rc.0 + - @rocket.chat/models@2.0.0-rc.0 + - @rocket.chat/core-services@0.12.0-rc.0 + - @rocket.chat/message-types@0.1.0-rc.0 + - @rocket.chat/federation-matrix@0.0.9-rc.0 + - @rocket.chat/gazzodown@26.0.0-rc.0 + - @rocket.chat/ui-client@26.0.0-rc.0 + - @rocket.chat/fuselage-ui-kit@26.0.0-rc.0 + - @rocket.chat/media-signaling@0.1.1-rc.0 + - @rocket.chat/ui-kit@0.39.0-rc.0 + - @rocket.chat/abac@0.1.0-rc.0 + - @rocket.chat/jwt@0.2.0-rc.0 + - @rocket.chat/tools@0.2.4-rc.0 + - @rocket.chat/presence@0.2.47-rc.0 + - @rocket.chat/logger@1.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.44-rc.0 + - @rocket.chat/api-client@0.2.47-rc.0 + - @rocket.chat/http-router@7.9.14-rc.0 + - @rocket.chat/license@1.1.7-rc.0 + - @rocket.chat/pdf-worker@0.3.26-rc.0 + - @rocket.chat/cron@0.1.47-rc.0 + - @rocket.chat/ui-avatar@22.0.0-rc.0 + - @rocket.chat/ui-theming@0.4.4 + - @rocket.chat/ui-video-conf@26.0.0-rc.0 + - @rocket.chat/omni-core-ee@0.0.12-rc.0 + - @rocket.chat/instance-status@0.1.47-rc.0 + - @rocket.chat/omni-core@0.0.12-rc.0 + - @rocket.chat/network-broker@0.2.26-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ ## 7.13.2 ### Patch Changes diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts index d4be080259cf4..cfe7b8ab7d4d4 100644 --- a/apps/meteor/app/2fa/server/code/index.ts +++ b/apps/meteor/app/2fa/server/code/index.ts @@ -9,6 +9,7 @@ import { EmailCheck } from './EmailCheck'; import type { ICodeCheck } from './ICodeCheck'; import { PasswordCheckFallback } from './PasswordCheckFallback'; import { TOTPCheck } from './TOTPCheck'; +import { normalizeHeaders } from '../../../lib/server/functions/getModifiedHttpHeaders'; import { settings } from '../../../settings/server'; export interface ITwoFactorOptions { @@ -184,9 +185,11 @@ export async function checkCodeForUser({ user, code, method, options = {}, conne throw new Meteor.Error('totp-user-not-found', 'TOTP User not found'); } - if (!code && !method && connection?.httpHeaders?.['x-2fa-code'] && connection.httpHeaders['x-2fa-method']) { - code = connection.httpHeaders['x-2fa-code']; - method = connection.httpHeaders['x-2fa-method']; + const headers = normalizeHeaders(connection?.httpHeaders); + + if (!code && !method && headers?.['x-2fa-code'] && headers['x-2fa-method']) { + code = headers['x-2fa-code']; + method = headers['x-2fa-method']; } if (connection && isAuthorizedForToken(connection, existingUser, options)) { diff --git a/apps/meteor/app/2fa/server/loginHandler.ts b/apps/meteor/app/2fa/server/loginHandler.ts index 1d039c7a89e4a..772e3cff93696 100644 --- a/apps/meteor/app/2fa/server/loginHandler.ts +++ b/apps/meteor/app/2fa/server/loginHandler.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { checkCodeForUser } from './code/index'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; const isMeteorError = (error: any): error is Meteor.Error => { return error?.meteorError !== undefined; diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index 24fc53910edb3..413a7de4bf1e9 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -28,7 +28,6 @@ import type { NotFoundResult, Operations, Options, - PartialThis, SuccessResult, TypedThis, TypedAction, @@ -37,6 +36,8 @@ import type { RedirectStatusCodes, RedirectResult, UnavailableResult, + GenericRouteExecutionContext, + TooManyRequestsResult, } from './definition'; import { getUserInfo } from './helpers/getUserInfo'; import { parseJsonQuery } from './helpers/parseJsonQuery'; @@ -156,12 +157,7 @@ const generateConnection = ( clientAddress: ipAddress, }); -export class APIClass< - TBasePath extends string = '', - TOperations extends { - [x: string]: unknown; - } = {}, -> { +export class APIClass = Record> { public typedRoutes: Record> = {}; protected apiPath?: string; @@ -170,7 +166,7 @@ export class APIClass< private _routes: { path: string; options: Options; endpoints: Record }[] = []; - public authMethods: ((...args: any[]) => any)[]; + public authMethods: ((routeContext: GenericRouteExecutionContext) => Promise)[]; protected helperMethods: Map any> = new Map(); @@ -248,11 +244,11 @@ export class APIClass< }; } - async parseJsonQuery(this: PartialThis) { - return parseJsonQuery(this); + async parseJsonQuery(routeContext: GenericRouteExecutionContext) { + return parseJsonQuery(routeContext); } - public addAuthMethod(func: (this: PartialThis, ...args: any[]) => any): void { + public addAuthMethod(func: (routeContext: GenericRouteExecutionContext) => Promise): void { this.authMethods.push(func); } @@ -388,7 +384,7 @@ export class APIClass< }; } - public tooManyRequests(msg?: string): { statusCode: number; body: Record & { success?: boolean } } { + public tooManyRequests(msg?: T): TooManyRequestsResult { return { statusCode: 429, body: { @@ -818,12 +814,21 @@ export class APIClass< } // Add a try/catch for each endpoint const originalAction = (operations[method as keyof Operations] as Record).action; + + if (options.deprecation && shouldBreakInVersion(options.deprecation.version)) { + throw new Meteor.Error('error-deprecated', `The endpoint ${route} should be removed`); + } + // eslint-disable-next-line @typescript-eslint/no-this-alias const api = this; (operations[method as keyof Operations] as Record).action = async function _internalRouteActionHandler() { + this.queryOperations = options.queryOperations; + this.queryFields = options.queryFields; + this.logger = logger; + if (options.authRequired || options.authOrAnonRequired) { - const user = await api.authenticatedRoute.call(this, this.request); + const user = await api.authenticatedRoute(this); this.user = user!; this.userId = this.user?._id; const authToken = this.request.headers.get('x-auth-token'); @@ -918,9 +923,7 @@ export class APIClass< connection: connection as unknown as IMethodConnection, })); - this.queryOperations = options.queryOperations; - (this as any).queryFields = options.queryFields; - this.parseJsonQuery = api.parseJsonQuery.bind(this as unknown as PartialThis); + this.parseJsonQuery = () => api.parseJsonQuery(this); result = (await DDP._CurrentInvocation.withValue(invocation as any, async () => originalAction.apply(this))) || api.success(); } catch (e: any) { @@ -972,12 +975,9 @@ export class APIClass< }); } - protected async authenticatedRoute(req: Request): Promise { - const headers = Object.fromEntries(req.headers.entries()); - - const { 'x-user-id': userId } = headers; - - const userToken = String(headers['x-auth-token']); + protected async authenticatedRoute(routeContext: GenericRouteExecutionContext): Promise { + const userId = routeContext.request.headers.get('x-user-id'); + const userToken = routeContext.request.headers.get('x-auth-token'); if (userId && userToken) { return Users.findOne( @@ -990,6 +990,16 @@ export class APIClass< }, ); } + + for (const method of this.authMethods) { + // eslint-disable-next-line no-await-in-loop -- we want serial execution + const user = await method(routeContext); + + if (user) { + return user; + } + } + return null; } diff --git a/apps/meteor/app/api/server/default/info.ts b/apps/meteor/app/api/server/default/info.ts index 8297f90fffd98..9f2a9d79e4a54 100644 --- a/apps/meteor/app/api/server/default/info.ts +++ b/apps/meteor/app/api/server/default/info.ts @@ -12,19 +12,3 @@ API.default.addRoute( }, }, ); - -API.default.addRoute( - 'ecdh_proxy/initEncryptedSession', - { authRequired: false }, - { - post() { - return { - statusCode: 200, - body: { - success: false, - error: 'Not Acceptable', - }, - }; - }, - }, -); diff --git a/apps/meteor/app/api/server/default/openApi.ts b/apps/meteor/app/api/server/default/openApi.ts index 28188302f570a..d59e6b8b08c22 100644 --- a/apps/meteor/app/api/server/default/openApi.ts +++ b/apps/meteor/app/api/server/default/openApi.ts @@ -6,8 +6,8 @@ import { WebApp } from 'meteor/webapp'; import swaggerUi from 'swagger-ui-express'; import { settings } from '../../../settings/server'; -import { Info } from '../../../utils/rocketchat.info'; import { API } from '../api'; +import { getTrimmedServerVersion } from '../lib/getTrimmedServerVersion'; const app = express(); @@ -46,7 +46,7 @@ const makeOpenAPIResponse = (paths: Record>) => ({ info: { title: 'Rocket.Chat API', description: 'Rocket.Chat API', - version: Info.version, + version: getTrimmedServerVersion(), }, servers: [ { diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 029c3ac223391..9684b99c798c8 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -56,6 +56,14 @@ export type ForbiddenResult = { }; }; +export type TooManyRequestsResult = { + statusCode: 429; + body: { + success: false; + error: T | 'Too many requests'; + }; +}; + export type InternalError = { statusCode: StatusCode; body: { @@ -142,21 +150,16 @@ export type SharedOptions = ( }; }; -export type PartialThis = { - user(bodyParams: Record, user: any): Promise; - readonly request: Request & { query: Record }; - readonly response: Response; - readonly userId: string; - readonly bodyParams: Record; - readonly path: string; - readonly queryParams: Record; - readonly queryOperations?: string[]; - readonly queryFields?: string[]; - readonly logger: Logger; - readonly route: string; -}; +export type GenericRouteExecutionContext = ActionThis; + +export type RouteExecutionContext = ActionThis< + TMethod, + TPathPattern, + TOptions +>; export type ActionThis = { + readonly logger: Logger; route: string; readonly requestIp: string; urlParams: UrlParams; @@ -183,6 +186,8 @@ export type ActionThis; /** diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index ea307c9f59844..bd7fc4f673dc0 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -1,10 +1,11 @@ import ejson from 'ejson'; import { Meteor } from 'meteor/meteor'; +import { isPlainObject } from '../../../../lib/utils/isPlainObject'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { API } from '../api'; -import type { PartialThis } from '../definition'; +import type { GenericRouteExecutionContext } from '../definition'; import { clean } from '../lib/cleanQuery'; import { isValidQuery } from '../lib/isValidQuery'; @@ -13,7 +14,7 @@ const pathAllowConf = { 'def': ['$or', '$and', '$regex'], }; -export async function parseJsonQuery(api: PartialThis): Promise<{ +export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise<{ sort: Record; /** * @deprecated To access "fields" parameter, use ALLOW_UNSAFE_QUERY_AND_FIELDS_API_PARAMS environment variable. @@ -24,10 +25,14 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ */ query: Record; }> { - const { userId, queryParams: params, logger, queryFields, queryOperations, response, route } = api; + const { userId = '', response, route, logger } = api; + + const params = isPlainObject(api.queryParams) ? api.queryParams : {}; + const queryFields = Array.isArray(api.queryFields) ? (api.queryFields as string[]) : []; + const queryOperations = Array.isArray(api.queryOperations) ? (api.queryOperations as string[]) : []; let sort; - if (params.sort) { + if (typeof params?.sort === 'string') { try { sort = JSON.parse(params.sort); Object.entries(sort).forEach(([key, value]) => { @@ -50,9 +55,9 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ `The usage of the "${parameter}" parameter in endpoint "${endpoint}" breaks the security of the API and can lead to data exposure. It has been deprecated and will be removed in the version ${version}.`; let fields: Record | undefined; - if (params.fields && isUnsafeQueryParamsAllowed) { + if (typeof params?.fields === 'string' && isUnsafeQueryParamsAllowed) { try { - apiDeprecationLogger.parameter(route, 'fields', '8.0.0', response, messageGenerator); + apiDeprecationLogger.parameter(route, 'fields', '9.0.0', response, messageGenerator); fields = JSON.parse(params.fields) as Record; Object.entries(fields).forEach(([key, value]) => { if (value !== 1 && value !== 0) { @@ -100,8 +105,8 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ } let query: Record = {}; - if (params.query && isUnsafeQueryParamsAllowed) { - apiDeprecationLogger.parameter(route, 'query', '8.0.0', response, messageGenerator); + if (typeof params?.query === 'string' && isUnsafeQueryParamsAllowed) { + apiDeprecationLogger.parameter(route, 'query', '9.0.0', response, messageGenerator); try { query = ejson.parse(params.query); query = clean(query, pathAllowConf.def); diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 971c0476ad5b0..77114be0b196e 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -9,6 +9,7 @@ import './helpers/parseJsonQuery'; import './default/info'; import './v1/assets'; import './v1/calendar'; +import './v1/call-history'; import './v1/channels'; import './v1/chat'; import './v1/cloud'; @@ -42,10 +43,6 @@ import './v1/banners'; import './v1/email-inbox'; import './v1/mailer'; import './v1/teams'; -import './v1/voip/extensions'; -import './v1/voip/queues'; -import './v1/voip/omnichannel'; -import './v1/voip'; import './v1/moderation'; // This has to come last so all endpoints are registered before generating the OpenAPI documentation diff --git a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts index 574519d928581..7337d70609755 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts @@ -1,29 +1,36 @@ import { expect } from 'chai'; -import { describe, it } from 'mocha'; +import { describe, it, before } from 'mocha'; import proxyquire from 'proxyquire'; import sinon from 'sinon'; const hasAllPermissionAsyncMock = sinon.stub(); const getCachedSupportedVersionsTokenMock = sinon.stub(); -const { getServerInfo } = proxyquire.noCallThru().load('./getServerInfo', { - '../../../utils/rocketchat.info': { - Info: { - version: '3.0.1', - }, - }, - '../../../authorization/server/functions/hasPermission': { - hasPermissionAsync: hasAllPermissionAsyncMock, - }, - '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken': { - getCachedSupportedVersionsToken: getCachedSupportedVersionsTokenMock, - }, - '../../../settings/server': { - settings: new Map(), - }, -}); // #ToDo: Fix those tests in a separate PR describe.skip('#getServerInfo()', () => { + let getServerInfo: any; + + before(() => { + const { getServerInfo: importedGetServerInfo } = proxyquire.noCallThru().load('./getServerInfo', { + '../../../utils/rocketchat.info': { + Info: { + version: '3.0.1', + }, + }, + '../../../authorization/server/functions/hasPermission': { + hasPermissionAsync: hasAllPermissionAsyncMock, + }, + '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken': { + getCachedSupportedVersionsToken: getCachedSupportedVersionsTokenMock, + }, + '../../../settings/server': { + settings: new Map(), + }, + }); + + getServerInfo = importedGetServerInfo; + }); + beforeEach(() => { hasAllPermissionAsyncMock.reset(); getCachedSupportedVersionsTokenMock.reset(); diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts index 020988b0aca42..4c65847c86ae6 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.ts @@ -1,5 +1,6 @@ import type { IWorkspaceInfo } from '@rocket.chat/core-typings'; +import { getTrimmedServerVersion } from './getTrimmedServerVersion'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { getCachedSupportedVersionsToken, @@ -8,15 +9,13 @@ import { import { settings } from '../../../settings/server'; import { Info, minimumClientVersions } from '../../../utils/rocketchat.info'; -const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1'); - export async function getServerInfo(userId?: string): Promise { const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics')); const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken()); const cloudWorkspaceId = settings.get('Cloud_Workspace_Id'); return { - version: removePatchInfo(Info.version), + version: getTrimmedServerVersion(), ...(hasPermissionToViewStatistics && { info: { ...Info, diff --git a/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts b/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts new file mode 100644 index 0000000000000..c0ceb9e880d5c --- /dev/null +++ b/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts @@ -0,0 +1,4 @@ +import { Info } from '../../../utils/rocketchat.info'; + +// Removes the patch version from the server version string +export const getTrimmedServerVersion = (): string => Info.version.replace(/(\d+\.\d+).*/, '$1'); diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index e1451f4dd6698..00dfc3e8f5292 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -131,6 +131,7 @@ type FindPaginatedUsersByStatusProps = { searchTerm: string; hasLoggedIn: boolean; type: string; + inactiveReason?: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; }; export async function findPaginatedUsersByStatus({ @@ -143,6 +144,7 @@ export async function findPaginatedUsersByStatus({ searchTerm, hasLoggedIn, type, + inactiveReason, }: FindPaginatedUsersByStatusProps) { const actualSort: Record = sort || { username: 1 }; if (sort?.status) { @@ -167,7 +169,6 @@ export async function findPaginatedUsersByStatus({ } const canSeeAllUserInfo = await hasPermissionAsync(uid, 'view-full-other-user-info'); - const canSeeExtension = canSeeAllUserInfo || (await hasPermissionAsync(uid, 'view-user-voip-extension')); const projection = { name: 1, @@ -181,7 +182,7 @@ export async function findPaginatedUsersByStatus({ type: 1, reason: 1, federated: 1, - ...(canSeeExtension ? { freeSwitchExtension: 1 } : {}), + freeSwitchExtension: 1, }; if (searchTerm?.trim()) { @@ -199,6 +200,26 @@ export async function findPaginatedUsersByStatus({ match.roles = { $in: roles }; } + if (inactiveReason) { + const inactiveReasonCondition = { + $or: [ + { inactiveReason: { $in: inactiveReason } }, + // This condition is to make it backward compatible with the old behavior + // The deactivated users not having the inactiveReason field should be returned as well + ...(inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long') + ? [{ inactiveReason: { $exists: false } }] + : []), + ], + }; + + if (match.$or) { + match.$and = [{ $or: match.$or }, inactiveReasonCondition]; + delete match.$or; + } else { + Object.assign(match, inactiveReasonCondition); + } + } + const { cursor, totalCount } = Users.findPaginated( { ...match, diff --git a/apps/meteor/app/api/server/middlewares/authentication.ts b/apps/meteor/app/api/server/middlewares/authentication.ts index e38da22fa793d..f2c7028a02601 100644 --- a/apps/meteor/app/api/server/middlewares/authentication.ts +++ b/apps/meteor/app/api/server/middlewares/authentication.ts @@ -27,7 +27,10 @@ export function authenticationMiddleware( if (userId && authToken) { req.user = (await Users.findOneByIdAndLoginToken(userId as string, hashLoginToken(authToken as string))) || undefined; } else { - req.user = (await oAuth2ServerAuth(req))?.user; + req.user = await oAuth2ServerAuth({ + headers: req.headers as Record, + query: req.query as Record, + }); } if (config.rejectUnauthorized && !req.user) { diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 5921932804b02..1f18ea4c47228 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -31,7 +31,7 @@ export class RocketChatAPIRouter< [x: string]: unknown; } = NonNullable, > extends Router { - protected convertActionToHandler(action: APIActionHandler): (c: HonoContext) => Promise> { + protected override convertActionToHandler(action: APIActionHandler): (c: HonoContext) => Promise> { return async (c: HonoContext): Promise> => { const { req, res } = c; const queryParams = this.parseQueryParams(req); diff --git a/apps/meteor/app/api/server/v1/banners.ts b/apps/meteor/app/api/server/v1/banners.ts index cecf9d4d9bad1..ff344362df6c5 100644 --- a/apps/meteor/app/api/server/v1/banners.ts +++ b/apps/meteor/app/api/server/v1/banners.ts @@ -1,73 +1,8 @@ import { Banner } from '@rocket.chat/core-services'; -import { isBannersDismissProps, isBannersGetNewProps, isBannersProps } from '@rocket.chat/rest-typings'; +import { isBannersDismissProps, isBannersProps } from '@rocket.chat/rest-typings'; import { API } from '../api'; -/** - * @deprecated - * @openapi - * /api/v1/banners.getNew: - * get: - * description: Gets the banners to be shown to the authenticated user - * deprecated: true - * security: - * $ref: '#/security/authenticated' - * parameters: - * - name: platform - * in: query - * description: The platform rendering the banner - * required: true - * schema: - * type: string - * enum: [web, mobile] - * example: web - * - name: bid - * in: query - * description: The id of a single banner - * required: false - * schema: - * type: string - * example: ByehQjC44FwMeiLbX - * responses: - * 200: - * description: The banners matching the criteria - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * banners: - * type: array - * items: - * $ref: '#/components/schemas/IBanner' - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'banners.getNew', - { - authRequired: true, - validateParams: isBannersGetNewProps, - deprecation: { version: '8.0.0', alternatives: ['/v1/banners/:id', '/v1/banners'] }, - }, - { - // deprecated - async get() { - const { platform, bid: bannerId } = this.queryParams; - - const banners = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined); - - return API.v1.success({ banners }); - }, - }, -); - /** * @openapi * /api/v1/banners/{id}: diff --git a/apps/meteor/app/api/server/v1/call-history.ts b/apps/meteor/app/api/server/v1/call-history.ts new file mode 100644 index 0000000000000..606632571ac91 --- /dev/null +++ b/apps/meteor/app/api/server/v1/call-history.ts @@ -0,0 +1,263 @@ +import type { CallHistoryItem, CallHistoryItemState, IMediaCall } from '@rocket.chat/core-typings'; +import { CallHistory, MediaCalls } from '@rocket.chat/models'; +import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; +import { + ajv, + validateNotFoundErrorResponse, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; + +import { ensureArray } from '../../../../lib/utils/arrayUtils'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; +import { API } from '../api'; +import { getPaginationItems } from '../helpers/getPaginationItems'; + +type CallHistoryList = PaginatedRequest<{ + filter?: string; + direction?: CallHistoryItem['direction']; + state?: CallHistoryItemState[] | CallHistoryItemState; +}>; + +const CallHistoryListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + sort: { + type: 'string', + }, + filter: { + type: 'string', + }, + direction: { + type: 'string', + enum: ['inbound', 'outbound'], + }, + state: { + // our clients serialize arrays as `state=value1&state=value2`, but if there's a single value the parser doesn't know it is an array, so we need to support both arrays and direct values + // if a client tries to send a JSON array, our parser will treat it as a string and the type validation will reject it + // This means this param won't work from Swagger UI + oneOf: [ + { + type: 'array', + items: { + $ref: '#/components/schemas/CallHistoryItemState', + }, + }, + { + $ref: '#/components/schemas/CallHistoryItemState', + }, + ], + }, + }, + required: [], + additionalProperties: false, +}; + +export const isCallHistoryListProps = ajv.compile(CallHistoryListSchema); + +const callHistoryListEndpoints = API.v1.get( + 'call-history.list', + { + response: { + 200: ajv.compile< + PaginatedResult<{ + items: CallHistoryItem[]; + }> + >({ + additionalProperties: false, + type: 'object', + properties: { + count: { + type: 'number', + description: 'The number of history items returned in this response.', + }, + offset: { + type: 'number', + description: 'The number of history items that were skipped in this response.', + }, + total: { + type: 'number', + description: 'The total number of history items that match the query.', + }, + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + items: { + type: 'array', + items: { + $ref: '#/components/schemas/CallHistoryItem', + }, + }, + }, + required: ['count', 'offset', 'total', 'items', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + query: isCallHistoryListProps, + authRequired: true, + }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams as Record); + const { sort } = await this.parseJsonQuery(); + + const { direction, state, filter } = this.queryParams; + + const filterText = typeof filter === 'string' && filter.trim(); + + const stateFilter = state && ensureArray(state); + const query = { + uid: this.userId, + ...(direction && { direction }), + ...(stateFilter?.length && { state: { $in: stateFilter } }), + ...(filterText && { + $or: [ + { + external: false, + contactName: { $regex: escapeRegExp(filterText), $options: 'i' }, + }, + { + external: false, + contactUsername: { $regex: escapeRegExp(filterText), $options: 'i' }, + }, + { + external: true, + contactExtension: { $regex: escapeRegExp(filterText), $options: 'i' }, + }, + ], + }), + }; + + const { cursor, totalCount } = CallHistory.findPaginated(query, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + const [items, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + items, + count: items.length, + offset, + total, + }); + }, +); + +type CallHistoryListEndpoints = ExtractRoutesFromAPI; + +type CallHistoryInfo = { historyId: string } | { callId: string }; + +const CallHistoryInfoSchema = { + oneOf: [ + { + type: 'object', + properties: { + historyId: { + type: 'string', + nullable: false, + }, + }, + required: ['historyId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + callId: { + type: 'string', + nullable: false, + }, + }, + required: ['callId'], + additionalProperties: false, + }, + ], +}; + +export const isCallHistoryInfoProps = ajv.compile(CallHistoryInfoSchema); + +const callHistoryInfoEndpoints = API.v1.get( + 'call-history.info', + { + response: { + 200: ajv.compile<{ + item: CallHistoryItem; + call?: IMediaCall; + }>({ + additionalProperties: false, + type: 'object', + properties: { + item: { + $ref: '#/components/schemas/CallHistoryItem', + description: 'The requested call history item.', + }, + call: { + type: 'object', + $ref: '#/components/schemas/IMediaCall', + description: 'The call information for the requested call history item.', + nullable: true, + }, + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + }, + required: ['item', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, + }, + query: isCallHistoryInfoProps, + authRequired: true, + }, + async function action() { + const { historyId, callId } = this.queryParams as Record & typeof this.queryParams; + + if (!historyId && !callId) { + return API.v1.failure(); + } + + const item = await (historyId + ? CallHistory.findOneByIdAndUid(historyId, this.userId) + : CallHistory.findOneByCallIdAndUid(callId, this.userId)); + + if (!item) { + return API.v1.notFound(); + } + + if (item.type === 'media-call' && item.callId) { + const call = await MediaCalls.findOneById(item.callId); + if (call) { + return API.v1.success({ + item, + call, + }); + } + } + + return API.v1.success({ item }); + }, +); + +type CallHistoryInfoEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends CallHistoryListEndpoints {} + + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends CallHistoryInfoEndpoints {} +} diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 64dc7d1f6f2e0..25978aefce3e8 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1,4 +1,3 @@ -import { Message } from '@rocket.chat/core-services'; import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { MessageTypes } from '@rocket.chat/message-types'; import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models'; @@ -21,7 +20,6 @@ import { isChatFollowMessageProps, isChatUnfollowMessageProps, isChatGetMentionedMessagesProps, - isChatOTRProps, isChatReactProps, isChatGetDeletedMessagesProps, isChatSyncThreadsListProps, @@ -41,7 +39,6 @@ import { messageSearch } from '../../../../server/methods/messageSearch'; import { getMessageHistory } from '../../../../server/publications/messages'; import { roomAccessAttributes } from '../../../authorization/server'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { deleteMessageValidatingPermission } from '../../../lib/server/functions/deleteMessage'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; @@ -437,7 +434,7 @@ API.v1.addRoute( } const sent = await applyAirGappedRestrictionsValidation(() => - executeSendMessage(this.userId, this.bodyParams.message as Pick, this.bodyParams.previewUrls), + executeSendMessage(this.userId, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), ); const [message] = await normalizeMessagesForUser([sent], this.userId); @@ -459,7 +456,7 @@ API.v1.addRoute( throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - await starMessage(this.userId, { + await starMessage(this.user, { _id: msg._id, rid: msg.rid, starred: true, @@ -481,7 +478,7 @@ API.v1.addRoute( throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - await starMessage(this.userId, { + await starMessage(this.user, { _id: msg._id, rid: msg.rid, starred: false, @@ -906,28 +903,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'chat.otr', - { authRequired: true, validateParams: isChatOTRProps }, - { - async post() { - const { roomId, type: otrType } = this.bodyParams; - - const { username, type } = this.user; - - if (!username) { - throw new Meteor.Error('error-invalid-user', 'Invalid user'); - } - - await canSendMessageAsync(roomId, { uid: this.userId, username, type }); - - await Message.saveSystemMessage(otrType, roomId, username, { _id: this.userId, username }); - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'chat.getURLPreview', { authRequired: true, validateParams: isChatGetURLPreviewProps }, diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 3686f2a7b9007..bfc70ba8b31a1 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -201,9 +201,6 @@ API.v1.addRoute( { authRequired: true, validateParams: ise2eUpdateGroupKeyParamsPOST, - deprecation: { - version: '8.0.0', - }, }, { async post() { diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 099d17141f8b4..b7251e67b1829 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -22,7 +22,7 @@ import { openRoom } from '../../../../server/lib/openRoom'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory'; @@ -385,12 +385,6 @@ API.v1.addRoute( ...(status && { status: { $in: status } }), }; - const canSeeExtension = await hasAtLeastOnePermissionAsync( - this.userId, - ['view-full-other-user-info', 'view-user-voip-extension'], - room._id, - ); - const options: FindOptions = { projection: { _id: 1, @@ -400,7 +394,7 @@ API.v1.addRoute( statusText: 1, utcOffset: 1, federated: 1, - ...(canSeeExtension && { freeSwitchExtension: 1 }), + freeSwitchExtension: 1, }, skip: offset, limit: count, @@ -416,8 +410,26 @@ API.v1.addRoute( const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + // find subscriptions of those users + const subs = await Subscriptions.findByRoomIdAndUserIds( + room._id, + members.map((member) => member._id), + { projection: { u: 1, status: 1, ts: 1, roles: 1 } }, + ).toArray(); + + const membersWithSubscriptionInfo = members.map((member) => { + const sub = subs.find((sub) => sub.u._id === member._id); + + const { u: _u, ...subscription } = sub || {}; + + return { + ...member, + subscription, + }; + }); + return API.v1.success({ - members, + members: membersWithSubscriptionInfo, count: members.length, offset, total, diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 5fb1781aa1411..78d77fe007f39 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -36,9 +36,9 @@ API.v1.addRoute( return API.v1.success({ integration: await addOutgoingIntegration(this.userId, this.bodyParams as INewOutgoingIntegration) }); case 'webhook-incoming': return API.v1.success({ integration: await addIncomingIntegration(this.userId, this.bodyParams as INewIncomingIntegration) }); + default: + return API.v1.failure('Invalid integration type.'); } - - return API.v1.failure('Invalid integration type.'); }, }, ); diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index ecdd8c58b052c..403ffcc29b4ce 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -23,7 +23,6 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { browseChannelsMethod } from '../../../../server/methods/browseChannels'; import { spotlightMethod } from '../../../../server/publications/spotlight'; import { resetAuditedSettingByUser, updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { getLogs } from '../../../../server/stream/stdout'; import { passwordPolicy } from '../../../lib/server'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; @@ -437,15 +436,6 @@ API.v1.addRoute( * schema: * $ref: '#/components/schemas/ApiFailureV1' */ -API.v1.addRoute( - 'stdout.queue', - { authRequired: true, permissionsRequired: ['view-logs'] }, - { - async get() { - return API.v1.success({ queue: getLogs() }); - }, - }, -); declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -535,7 +525,8 @@ API.v1.addRoute( if (settings.get('Log_Level') === '2') { Meteor._debug(`Exception while invoking method ${method}`, err); } - return API.v1.success(mountResult({ id, error: err })); + + return API.v1.failure(mountResult({ id, error: err })); } }, }, @@ -590,7 +581,7 @@ API.v1.addRoute( if (settings.get('Log_Level') === '2') { Meteor._debug(`Exception while invoking method ${method}`, err); } - return API.v1.success(mountResult({ id, error: err })); + return API.v1.failure(mountResult({ id, error: err })); } }, }, diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts index 3af8b8002b532..26a5bd0cfd482 100644 --- a/apps/meteor/app/api/server/v1/oauthapps.ts +++ b/apps/meteor/app/api/server/v1/oauthapps.ts @@ -8,7 +8,6 @@ import { } from '@rocket.chat/rest-typings'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { addOAuthApp } from '../../../oauth2-server-config/server/admin/functions/addOAuthApp'; import { deleteOAuthApp } from '../../../oauth2-server-config/server/admin/methods/deleteOAuthApp'; import { updateOAuthApp } from '../../../oauth2-server-config/server/admin/methods/updateOAuthApp'; @@ -88,7 +87,7 @@ const UpdateOAuthAppParamsSchema = { const isUpdateOAuthAppParams = ajv.compile(UpdateOAuthAppParamsSchema); -type OauthAppsGetParams = { clientId: string } | { appId: string } | { _id: string }; +type OauthAppsGetParams = { clientId: string } | { _id: string }; const oauthAppsGetParamsSchema = { oneOf: [ @@ -112,16 +111,6 @@ const oauthAppsGetParamsSchema = { required: ['clientId'], additionalProperties: false, }, - { - type: 'object', - properties: { - appId: { - type: 'string', - }, - }, - required: ['appId'], - additionalProperties: false, - }, ], }; @@ -292,10 +281,6 @@ const oauthAppsEndpoints = API.v1 return API.v1.failure('OAuth app not found.'); } - if ('appId' in this.queryParams) { - apiDeprecationLogger.parameter(this.route, 'appId', '7.0.0', this.response); - } - return API.v1.success({ oauthApp, }); diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 10c3a9fd78389..8985e48c0aff5 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -10,7 +10,6 @@ import { getUsersInRolePaginated } from '../../../authorization/server/functions import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync, hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { addUserToRole } from '../../../authorization/server/methods/addUserToRole'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnRoleChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server/index'; import type { ExtractRoutesFromAPI } from '../ApiClass'; @@ -64,17 +63,13 @@ API.v1.addRoute( } const user = await getUserFromParams(this.bodyParams); - const { roleId, roleName, roomId } = this.bodyParams; + const { roleId, roomId } = this.bodyParams; if (!roleId) { - if (!roleName) { - return API.v1.failure('error-invalid-role-properties'); - } - - apiDeprecationLogger.parameter(this.route, 'roleName', '7.0.0', this.response); + return API.v1.failure('error-invalid-role-properties'); } - const role = roleId ? await Roles.findOneById(roleId) : await Roles.findOneByIdOrName(roleName as string); + const role = await Roles.findOneById(roleId); if (!role) { return API.v1.failure('error-role-not-found', 'Role not found'); } @@ -117,21 +112,10 @@ API.v1.addRoute( } const options = { projection: { _id: 1 } }; - let roleData = await Roles.findOneById>(role, options); - if (!roleData) { - roleData = await Roles.findOneByName>(role, options); - if (!roleData) { - throw new Meteor.Error('error-invalid-roleId'); - } + const roleData = await Roles.findOneById>(role, options); - apiDeprecationLogger.deprecatedParameterUsage( - this.route, - 'role', - '7.0.0', - this.response, - ({ parameter, endpoint, version }) => - `Querying \`${parameter}\` by name is deprecated in ${endpoint} and will be removed on the removed on version ${version}`, - ); + if (!roleData) { + throw new Meteor.Error('error-invalid-roleId'); } const { cursor, totalCount } = await getUsersInRolePaginated(roleData._id, roomId, { @@ -191,14 +175,10 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); } - const { roleId, roleName, username, scope } = bodyParams; + const { roleId, username, scope } = bodyParams; if (!roleId) { - if (!roleName) { - return API.v1.failure('error-invalid-role-properties'); - } - - apiDeprecationLogger.parameter(this.route, 'roleName', '7.0.0', this.response); + return API.v1.failure('error-invalid-role-properties'); } const user = await Users.findOneByUsername(username); @@ -207,7 +187,7 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); } - const role = roleId ? await Roles.findOneById(roleId) : await Roles.findOneByIdOrName(roleName as string); + const role = await Roles.findOneById(roleId); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 88558dc9a62df..068bcdf266500 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -1,4 +1,4 @@ -import { Media, MeteorError, Team } from '@rocket.chat/core-services'; +import { FederationMatrix, Media, MeteorError, Team } from '@rocket.chat/core-services'; import type { IRoom, IUpload } from '@rocket.chat/core-typings'; import { isPrivateRoom, isPublicRoom } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users, Uploads, Subscriptions } from '@rocket.chat/models'; @@ -15,10 +15,14 @@ import { isRoomsMembersOrderedByRoleProps, isRoomsChangeArchivationStateProps, isRoomsHideProps, + isRoomsInviteProps, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; +import { adminFields } from '../../../../lib/rooms/adminFields'; import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/lib/eraseRoom'; @@ -184,74 +188,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.upload/:rid', - { - authRequired: true, - deprecation: { - version: '8.0.0', - alternatives: ['/v1/rooms.media/:rid'], - }, - }, - { - async post() { - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { - return API.v1.forbidden(); - } - - const file = await getUploadFormData( - { - request: this.request, - }, - { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, - ); - - if (!file) { - throw new Meteor.Error('invalid-field'); - } - - const { fields } = file; - let { fileBuffer } = file; - - const details = { - name: file.filename, - size: fileBuffer.length, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - }; - - const stripExif = settings.get('Message_Attachments_Strip_Exif'); - if (stripExif) { - // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) - fileBuffer = await Media.stripExifFromBuffer(fileBuffer); - details.size = fileBuffer.length; - } - - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, fileBuffer); - - if ((fields.description?.length ?? 0) > settings.get('Message_MaxAllowedSize')) { - throw new Meteor.Error('error-message-size-exceeded'); - } - - uploadedFile.description = fields.description; - - delete fields.description; - - await applyAirGappedRestrictionsValidation(() => - sendFileMessage(this.userId, { roomId: this.urlParams.rid, file: uploadedFile, msgData: fields }), - ); - - const message = await Messages.getMessageByFileIdAndUsername(uploadedFile._id, this.userId); - - return API.v1.success({ - message, - }); - }, - }, -); - API.v1.addRoute( 'rooms.media/:rid', { authRequired: true }, @@ -1029,51 +965,152 @@ const isRoomGetRolesPropsSchema = { additionalProperties: false, required: ['rid'], }; -export const roomEndpoints = API.v1.get( - 'rooms.roles', - { - authRequired: true, - query: ajv.compile<{ - rid: string; - }>(isRoomGetRolesPropsSchema), - response: { - 200: ajv.compile<{ - roles: RoomRoles[]; +export const roomEndpoints = API.v1 + .get( + 'rooms.roles', + { + authRequired: true, + query: ajv.compile<{ + rid: string; + }>(isRoomGetRolesPropsSchema), + response: { + 200: ajv.compile<{ + roles: RoomRoles[]; + }>({ + type: 'object', + properties: { + roles: { + type: 'array', + items: { + type: 'object', + properties: { + rid: { type: 'string' }, + u: { + type: 'object', + properties: { _id: { type: 'string' }, username: { type: 'string' } }, + required: ['_id', 'username'], + }, + roles: { type: 'array', items: { type: 'string' } }, + }, + required: ['rid', 'u', 'roles'], + }, + }, + }, + required: ['roles'], + }), + }, + }, + async function () { + const { rid } = this.queryParams; + const roles = await executeGetRoomRoles(rid, this.userId); + + return API.v1.success({ + roles, + }); + }, + ) + .get( + 'rooms.adminRooms.privateRooms', + { + authRequired: true, + permissionsRequired: ['view-room-administration'], + query: ajv.compile<{ + filter?: string; + offset?: number; + count?: number; + sort?: string; }>({ type: 'object', properties: { - roles: { - type: 'array', - items: { - type: 'object', - properties: { - rid: { type: 'string' }, - u: { - type: 'object', - properties: { _id: { type: 'string' }, username: { type: 'string' } }, - required: ['_id', 'username'], - }, - roles: { type: 'array', items: { type: 'string' } }, - }, - required: ['rid', 'u', 'roles'], + filter: { type: 'string' }, + offset: { type: 'number' }, + count: { type: 'number' }, + sort: { type: 'string' }, + }, + additionalProperties: true, + }), + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ + rooms: IRoom[]; + count: number; + offset: number; + total: number; + }>({ + type: 'object', + properties: { + rooms: { + type: 'array', + items: { type: 'object' }, }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, }, + required: ['rooms', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + const { filter } = this.queryParams; + + const name = (filter || '').trim(); + + const { cursor, totalCount } = Rooms.findPrivateRoomsAndTeamsPaginated(name, { + skip: offset, + limit: count, + sort: sort || { default: -1, name: 1 }, + projection: adminFields, + }); + + const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + rooms, + count: rooms.length, + offset, + total, + }); + }, + ); + +const roomInviteEndpoints = API.v1.post( + 'rooms.invite', + { + authRequired: true, + body: isRoomsInviteProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, }, - required: ['roles'], + required: ['success'], + additionalProperties: false, }), }, }, - async function () { - const { rid } = this.queryParams; - const roles = await executeGetRoomRoles(rid, this.userId); + async function action() { + const { roomId, action } = this.bodyParams; - return API.v1.success({ - roles, - }); + try { + await FederationMatrix.handleInvite(roomId, this.userId, action); + return API.v1.success(); + } catch (error) { + return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); + } }, ); -type RoomEndpoints = ExtractRoutesFromAPI; +type RoomEndpoints = ExtractRoutesFromAPI & ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index a3192aeb9ec14..b6e406bbfc969 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { isSubscriptionsGetProps, isSubscriptionsGetOneProps, @@ -85,7 +85,12 @@ API.v1.addRoute( const { readThreads = false } = this.bodyParams; const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId; - await readMessages(roomId, this.userId, readThreads); + const room = await Rooms.findOneById(roomId); + if (!room) { + throw new Error('error-invalid-subscription'); + } + + await readMessages(room, this.userId, readThreads); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 9ce577522a2d9..f1893def4a7a2 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -20,6 +20,7 @@ import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; +import { settings } from '../../../settings/server'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { eraseTeam } from '../lib/eraseTeam'; @@ -235,6 +236,13 @@ API.v1.addRoute( } const canUpdateAny = !!(await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId)); + if (settings.get('ABAC_Enabled') && isDefault) { + const room = await Rooms.findOneByIdAndType(roomId, 'p', { projection: { abacAttributes: 1 } }); + if (room?.abacAttributes?.length) { + return API.v1.failure('error-room-is-abac-managed'); + } + } + const room = await Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny); return API.v1.success({ room }); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index f6058085deeec..e567970508438 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,6 +1,6 @@ import { MeteorError, Team, api, Calendar } from '@rocket.chat/core-services'; import { type IExportOperation, type ILoginToken, type IPersonalAccessToken, type IUser, type UserStatus } from '@rocket.chat/core-typings'; -import { Users, Subscriptions } from '@rocket.chat/models'; +import { Users, Subscriptions, Sessions } from '@rocket.chat/models'; import { isUserCreateParamsPOST, isUserSetActiveStatusParamsPOST, @@ -18,6 +18,7 @@ import { isUsersSetPreferencesParamsPOST, isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, + ajv, } from '@rocket.chat/rest-typings'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; @@ -69,6 +70,7 @@ import { deleteUserOwnAccount } from '../../../lib/server/methods/deleteUserOwnA import { settings } from '../../../settings/server'; import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams } from '../helpers/getUserFromParams'; @@ -326,7 +328,7 @@ API.v1.addRoute( validateCustomFields(this.bodyParams.customFields); } - if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.userId, this.bodyParams.freeSwitchExtension))) { + if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.bodyParams.freeSwitchExtension))) { return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed'); } @@ -616,7 +618,7 @@ API.v1.addRoute( const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const { status, hasLoggedIn, type, roles, searchTerm } = this.queryParams; + const { status, hasLoggedIn, type, roles, searchTerm, inactiveReason } = this.queryParams; return API.v1.success( await findPaginatedUsersByStatus({ @@ -629,6 +631,7 @@ API.v1.addRoute( searchTerm, hasLoggedIn, type, + inactiveReason, }), ); }, @@ -760,17 +763,70 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const usersEndpoints = API.v1.post( 'users.createToken', - { authRequired: true, deprecationVersion: '8.0.0' }, { - async post() { - const user = await getUserFromParams(this.bodyParams); + authRequired: true, + body: ajv.compile<{ userId: string; secret: string }>({ + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + secret: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId', 'secret'], + additionalProperties: false, + }), + response: { + 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ + type: 'object', + properties: { + data: { + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + authToken: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['data', 'success'], + additionalProperties: false, + }), + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const user = await getUserFromParams(this.bodyParams); - const data = await generateAccessToken(this.userId, user._id); + const data = await generateAccessToken(user._id, this.bodyParams.secret); - return data ? API.v1.success({ data }) : API.v1.forbidden(); - }, + return API.v1.success({ data }); }, ); @@ -1282,6 +1338,8 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } + await Sessions.logoutAllByUserId(userId, this.userId); + void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [] } }); return API.v1.success({ @@ -1355,18 +1413,17 @@ API.v1.addRoute( } const { _id, username, roles, name } = user; - let { statusText } = user; - - // TODO refactor to not update the user twice (one inside of `setStatusText` and then later just the status + statusDefault) + let { statusText, status } = user; if (this.bodyParams.message || this.bodyParams.message === '') { - await setStatusText(user._id, this.bodyParams.message); + await setStatusText(user._id, this.bodyParams.message, { emit: false }); statusText = this.bodyParams.message; } + if (this.bodyParams.status) { const validStatus = ['online', 'away', 'offline', 'busy']; if (validStatus.includes(this.bodyParams.status)) { - const { status } = this.bodyParams; + status = this.bodyParams.status; if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { @@ -1384,11 +1441,6 @@ API.v1.addRoute( }, ); - void api.broadcast('presence.status', { - user: { status, _id, username, statusText, roles, name }, - previousStatus: user.status, - }); - void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); } else { throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { @@ -1397,6 +1449,11 @@ API.v1.addRoute( } } + void api.broadcast('presence.status', { + user: { status, _id, username, statusText, roles, name }, + previousStatus: user.status, + }); + return API.v1.success(); }, }, @@ -1439,3 +1496,10 @@ settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); }); + +type UsersEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends UsersEndpoints {} +} diff --git a/apps/meteor/app/api/server/v1/voip/events.ts b/apps/meteor/app/api/server/v1/voip/events.ts deleted file mode 100644 index 7a2c9288b4c4b..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/events.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LivechatVoip } from '@rocket.chat/core-services'; -import { VoipClientEvents } from '@rocket.chat/core-typings'; -import { VoipRoom } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; - -import { canAccessRoomAsync } from '../../../../authorization/server'; -import { API } from '../../api'; - -API.v1.addRoute( - 'voip/events', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async post() { - check(this.bodyParams, { - event: Match.Where((v: string) => { - return Object.values(VoipClientEvents).includes(v); - }), - rid: String, - comment: Match.Maybe(String), - }); - - const { rid, event, comment } = this.bodyParams; - - const room = await VoipRoom.findOneVoipRoomById(rid); - if (!room) { - return API.v1.notFound(); - } - if (!(await canAccessRoomAsync(room, this.user))) { - return API.v1.forbidden(); - } - - return API.v1.success(await LivechatVoip.handleEvent(event, room, this.user, comment)); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/voip/extensions.ts b/apps/meteor/app/api/server/v1/voip/extensions.ts deleted file mode 100644 index fd2181d97f0d9..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/extensions.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { VoipAsterisk } from '@rocket.chat/core-services'; -import type { IVoipExtensionBase } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; - -import { logger } from './logger'; -import { settings } from '../../../../settings/server'; -import { generateJWT } from '../../../../utils/server/lib/JWTHelper'; -import { API } from '../../api'; - -// Get the connector version and type -API.v1.addRoute( - 'connector.getVersion', - { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, - { - async get() { - const version = await VoipAsterisk.getConnectorVersion(); - return API.v1.success(version); - }, - }, -); - -// Get the extensions available on the call server -API.v1.addRoute( - 'connector.extension.list', - { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, - { - async get() { - const list = await VoipAsterisk.getExtensionList(); - const result = list.result as IVoipExtensionBase[]; - return API.v1.success({ extensions: result }); - }, - }, -); - -/* Get the details of a single extension. - * Note : This API will either be called by the endpoint - * or will be consumed internally. - */ -API.v1.addRoute( - 'connector.extension.getDetails', - { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - extension: String, - }), - ); - const endpointDetails = await VoipAsterisk.getExtensionDetails(this.queryParams); - return API.v1.success({ ...endpointDetails.result }); - }, - }, -); - -/* Get the details for registration extension. - */ - -API.v1.addRoute( - 'connector.extension.getRegistrationInfoByExtension', - { authRequired: true, permissionsRequired: ['manage-voip-call-settings'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - extension: String, - }), - ); - const endpointDetails = await VoipAsterisk.getRegistrationInfo(this.queryParams); - const encKey = settings.get('VoIP_JWT_Secret'); - if (!encKey) { - logger.warn('No JWT keys set. Sending registration info as plain text'); - return API.v1.success({ ...endpointDetails.result }); - } - - const result = generateJWT(endpointDetails.result, encKey); - return API.v1.success({ result }); - }, - }, -); - -API.v1.addRoute( - 'connector.extension.getRegistrationInfoByUserId', - { authRequired: true, permissionsRequired: ['view-agent-extension-association'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - id: String, - }), - ); - const { id } = this.queryParams; - - if (id !== this.userId) { - return API.v1.forbidden(); - } - - const { extension } = - (await Users.getVoipExtensionByUserId(id, { - projection: { - _id: 1, - username: 1, - extension: 1, - }, - })) || {}; - - if (!extension) { - return API.v1.notFound('Extension not found'); - } - - const endpointDetails = await VoipAsterisk.getRegistrationInfo({ extension }); - const encKey = settings.get('VoIP_JWT_Secret'); - if (!encKey) { - logger.warn('No JWT keys set. Sending registration info as plain text'); - return API.v1.success({ ...endpointDetails.result }); - } - - const result = generateJWT(endpointDetails.result, encKey); - return API.v1.success({ result }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/voip/index.ts b/apps/meteor/app/api/server/v1/voip/index.ts deleted file mode 100644 index 95b8dd15a9386..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './extensions'; -import './queues'; -import './events'; -import './rooms'; -import './server-connection'; diff --git a/apps/meteor/app/api/server/v1/voip/logger.ts b/apps/meteor/app/api/server/v1/voip/logger.ts deleted file mode 100644 index fffa680784803..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; - -export const logger = new Logger('VoIP'); diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts deleted file mode 100644 index abd92b9fa5899..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { LivechatVoip } from '@rocket.chat/core-services'; -import type { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; - -import { logger } from './logger'; -import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; -import { API } from '../../api'; -import { getPaginationItems } from '../../helpers/getPaginationItems'; - -function filter( - array: IVoipExtensionWithAgentInfo[], - { queues, extension, agentId, status }: { queues?: string[]; extension?: string; agentId?: string; status?: string }, -): IVoipExtensionWithAgentInfo[] { - const defaultFunc = (): boolean => true; - return array.filter((item) => { - const queuesCond = queues && Array.isArray(queues) ? (): boolean => item.queues?.some((q) => queues.includes(q)) || false : defaultFunc; - const extensionCond = extension?.trim() ? (): boolean => item?.extension === extension : defaultFunc; - const agentIdCond = agentId?.trim() ? (): boolean => item?.userId === agentId : defaultFunc; - const statusCond = status?.trim() ? (): boolean => item?.state === status : defaultFunc; - - return queuesCond() && extensionCond() && agentIdCond() && statusCond(); - }); -} - -function paginate(array: T[], count = 10, offset = 0): T[] { - return array.slice(offset, offset + count); -} - -const isUserAndExtensionParams = (p: any): p is { userId: string; extension: string } => p.userId && p.extension; -const isUserIdndTypeParams = (p: any): p is { userId: string; type: 'free' | 'allocated' | 'available' } => p.userId && p.type; - -API.v1.addRoute( - 'omnichannel/agent/extension', - { authRequired: true, permissionsRequired: ['manage-agent-extension-association'] }, - { - async post() { - check( - this.bodyParams, - Match.OneOf( - Match.ObjectIncluding({ - username: String, - extension: String, - }), - Match.ObjectIncluding({ - userId: String, - extension: String, - }), - ), - ); - - const { extension } = this.bodyParams; - let user: IUser | null = null; - - if (!isUserAndExtensionParams(this.bodyParams)) { - if (!this.bodyParams.username) { - return API.v1.notFound(); - } - user = await Users.findOneByAgentUsername(this.bodyParams.username, { - projection: { - _id: 1, - username: 1, - }, - }); - } else { - if (!this.bodyParams.userId) { - return API.v1.notFound(); - } - user = await Users.findOneAgentById(this.bodyParams.userId, { - projection: { - _id: 1, - username: 1, - }, - }); - } - - if (!user) { - return API.v1.notFound('User not found or does not have livechat-agent role'); - } - - try { - await Users.setExtension(user._id, extension); - - void notifyOnUserChange({ - clientAction: 'updated', - id: user._id, - diff: { - extension, - }, - }); - - return API.v1.success(); - } catch (e) { - logger.error({ msg: 'Extension already in use' }); - return API.v1.failure(`extension already in use ${extension}`); - } - }, - }, -); - -API.v1.addRoute( - 'omnichannel/agent/extension/:username', - { - authRequired: true, - permissionsRequired: { - GET: ['view-agent-extension-association'], - DELETE: ['manage-agent-extension-association'], - }, - }, - { - // Get the extensions associated with the agent passed as request params. - async get() { - check( - this.urlParams, - Match.ObjectIncluding({ - username: String, - }), - ); - const { username } = this.urlParams; - const user = await Users.findOneByAgentUsername(username, { - projection: { _id: 1 }, - }); - if (!user) { - return API.v1.notFound('User not found'); - } - const extension = await Users.getVoipExtensionByUserId(user._id, { - projection: { - _id: 1, - username: 1, - extension: 1, - }, - }); - if (!extension) { - return API.v1.notFound('Extension not found'); - } - return API.v1.success({ extension }); - }, - - async delete() { - check( - this.urlParams, - Match.ObjectIncluding({ - username: String, - }), - ); - const { username } = this.urlParams; - const user = await Users.findOneByAgentUsername(username, { - projection: { - _id: 1, - username: 1, - extension: 1, - }, - }); - if (!user) { - return API.v1.notFound(); - } - if (!user.extension) { - return API.v1.success(); - } - - logger.debug(`Removing extension association for user ${user._id} (extension was ${user.extension})`); - await Users.unsetExtension(user._id); - - void notifyOnUserChange({ - clientAction: 'updated', - id: user._id, - diff: { - extension: null, - }, - }); - - return API.v1.success(); - }, - }, -); - -// Get free extensions -API.v1.addRoute( - 'omnichannel/extension', - { authRequired: true, permissionsRequired: ['manage-agent-extension-association'] }, - { - async get() { - check( - this.queryParams, - Match.OneOf( - Match.ObjectIncluding({ - type: Match.OneOf('free', 'allocated', 'available'), - userId: String, - }), - Match.ObjectIncluding({ - type: Match.OneOf('free', 'allocated', 'available'), - username: String, - }), - ), - ); - - switch (this.queryParams.type.toLowerCase()) { - case 'free': { - const extensions = await LivechatVoip.getFreeExtensions(); - if (!extensions) { - return API.v1.failure('Error in finding free extensons'); - } - return API.v1.success({ extensions }); - } - case 'allocated': { - const extensions = await LivechatVoip.getExtensionAllocationDetails(); - if (!extensions) { - return API.v1.failure('Error in allocated extensions'); - } - return API.v1.success({ extensions: extensions.map((e) => e.extension) }); - } - case 'available': { - let user: IUser | null = null; - if (!isUserIdndTypeParams(this.queryParams)) { - user = await Users.findOneByAgentUsername(this.queryParams.username, { - projection: { _id: 1, extension: 1 }, - }); - } else { - user = await Users.findOneAgentById(this.queryParams.userId, { - projection: { _id: 1, extension: 1 }, - }); - } - - const freeExt = await LivechatVoip.getFreeExtensions(); - const extensions = user?.extension ? [user.extension, ...freeExt] : freeExt; - return API.v1.success({ extensions }); - } - default: - return API.v1.notFound(`${this.queryParams.type} not found `); - } - }, - }, -); - -API.v1.addRoute( - 'omnichannel/extensions', - { authRequired: true, permissionsRequired: ['manage-agent-extension-association'] }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { status, agentId, queues, extension } = this.queryParams; - - check(status, Match.Maybe(String)); - check(agentId, Match.Maybe(String)); - check(queues, Match.Maybe([String])); - check(extension, Match.Maybe(String)); - - const extensions = await LivechatVoip.getExtensionListWithAgentData(); - const filteredExts = filter(extensions, { - status: status ?? undefined, - agentId: agentId ?? undefined, - queues: queues ?? undefined, - extension: extension ?? undefined, - }); - - // paginating in memory as Asterisk doesn't provide pagination for commands - return API.v1.success({ - extensions: paginate(filteredExts, count, offset), - offset, - count, - total: filteredExts.length, - }); - }, - }, -); - -API.v1.addRoute( - 'omnichannel/agents/available', - { authRequired: true, permissionsRequired: ['manage-agent-extension-association'] }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { text, includeExtension = '' } = this.queryParams; - - const { agents, total } = await LivechatVoip.getAvailableAgents(includeExtension, text, count, offset, sort); - - return API.v1.success({ - agents, - offset, - count, - total, - }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/voip/queues.ts b/apps/meteor/app/api/server/v1/voip/queues.ts deleted file mode 100644 index 4a8fb86be0e39..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/queues.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { VoipAsterisk } from '@rocket.chat/core-services'; -import type { IVoipConnectorResult, IQueueSummary, IQueueMembershipDetails, IQueueMembershipSubscription } from '@rocket.chat/core-typings'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../api'; - -API.v1.addRoute( - 'voip/queues.getSummary', - { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, - { - async get() { - const queueSummary = await VoipAsterisk.getQueueSummary(); - return API.v1.success({ summary: queueSummary.result as IQueueSummary[] }); - }, - }, -); - -API.v1.addRoute( - 'voip/queues.getQueuedCallsForThisExtension', - { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - extension: String, - }), - ); - const membershipDetails: IVoipConnectorResult = await VoipAsterisk.getQueuedCallsForThisExtension(this.queryParams); - return API.v1.success(membershipDetails.result as IQueueMembershipDetails); - }, - }, -); - -API.v1.addRoute( - 'voip/queues.getMembershipSubscription', - { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - extension: String, - }), - ); - const membershipDetails: IVoipConnectorResult = await VoipAsterisk.getQueueMembership(this.queryParams); - return API.v1.success(membershipDetails.result as IQueueMembershipSubscription); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/voip/rooms.ts b/apps/meteor/app/api/server/v1/voip/rooms.ts deleted file mode 100644 index 13d2e2c31a054..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/rooms.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { LivechatVoip } from '@rocket.chat/core-services'; -import type { IVoipRoom } from '@rocket.chat/core-typings'; -import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings'; - -import { typedJsonParse } from '../../../../../lib/typedJSONParse'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { API } from '../../api'; -import { getPaginationItems } from '../../helpers/getPaginationItems'; - -type DateParam = { start?: string; end?: string }; -const parseDateParams = (date?: string): DateParam => { - return date && typeof date === 'string' ? typedJsonParse(date) : {}; -}; -const validateDateParams = (property: string, date: DateParam = {}): DateParam => { - if (date?.start && isNaN(Date.parse(date.start))) { - throw new Error(`The "${property}.start" query parameter must be a valid date.`); - } - if (date?.end && isNaN(Date.parse(date.end))) { - throw new Error(`The "${property}.end" query parameter must be a valid date.`); - } - return date; -}; -const parseAndValidate = (property: string, date?: string): DateParam => { - return validateDateParams(property, parseDateParams(date)); -}; - -/** - * @openapi - * /voip/server/api/v1/voip/room - * get: - * description: Creates a new room if rid is not passed, else gets an existing room - * based on rid and token . This configures the rate limit. An average call volume in a contact - * center is 600 calls a day - * considering 8 hour shift. Which comes to 1.25 calls per minute. - * we will keep the safe limit which is 5 calls a minute. - * security: - * parameters: - * - name: token - * in: query - * description: The visitor token - * required: true - * schema: - * type: string - * example: ByehQjC44FwMeiLbX - * - name: rid - * in: query - * description: The room id - * required: false - * schema: - * type: string - * example: ByehQjC44FwMeiLbX - * - name: agentId - * in: query - * description: Agent Id - * required: false - * schema: - * type: string - * example: ByehQjC44FwMeiLbX - * responses: - * 200: - * description: Room object and flag indicating whether a new room is created. - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * room: - * type: object - * items: - * $ref: '#/components/schemas/IRoom' - * newRoom: - * type: boolean - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ - -const isRoomSearchProps = (props: any): props is { rid: string; token: string } => { - return 'rid' in props && 'token' in props; -}; - -const isRoomCreationProps = (props: any): props is { agentId: string; direction: IVoipRoom['direction'] } => { - return 'agentId' in props && 'direction' in props; -}; - -API.v1.addRoute( - 'voip/room', - { - authRequired: true, - rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000 }, - permissionsRequired: ['inbound-voip-calls'], - validateParams: isVoipRoomProps, - }, - { - async get() { - const { token } = this.queryParams; - let agentId: string | undefined = undefined; - let direction: IVoipRoom['direction'] = 'inbound'; - let rid: string | undefined = undefined; - - if (isRoomCreationProps(this.queryParams)) { - agentId = this.queryParams.agentId; - direction = this.queryParams.direction; - } - - if (isRoomSearchProps(this.queryParams)) { - rid = this.queryParams.rid; - } - - const guest = await LivechatVisitors.getVisitorByToken(token, {}); - if (!guest) { - return API.v1.failure('invalid-token'); - } - - if (!rid) { - const room = await VoipRoom.findOneOpenByVisitorToken(token, { projection: API.v1.defaultFieldsToExclude }); - if (room) { - return API.v1.success({ room, newRoom: false }); - } - if (!agentId) { - return API.v1.failure('agent-not-found'); - } - - const agentObj = await Users.findOneAgentById(agentId, { - projection: { username: 1 }, - }); - if (!agentObj?.username) { - return API.v1.failure('agent-not-found'); - } - - const { username, _id } = agentObj; - const agent = { agentId: _id, username }; - const rid = Random.id(); - - return API.v1.success( - await LivechatVoip.getNewRoom(guest, agent, rid, direction, { - projection: API.v1.defaultFieldsToExclude, - }), - ); - } - - const room = await VoipRoom.findOneByIdAndVisitorToken(rid, token, { projection: API.v1.defaultFieldsToExclude }); - if (!room) { - return API.v1.failure('invalid-room'); - } - return API.v1.success({ room, newRoom: false }); - }, - }, -); - -API.v1.addRoute( - 'voip/rooms', - { authRequired: true, validateParams: isVoipRoomsProps }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - - const { sort, fields } = await this.parseJsonQuery(); - const { agents, open, tags, queue, visitorId, direction, roomName } = this.queryParams; - const { createdAt: createdAtParam, closedAt: closedAtParam } = this.queryParams; - - // Reusing same L room permissions for simplicity - const hasAdminAccess = await hasPermissionAsync(this.userId, 'view-livechat-rooms'); - const hasAgentAccess = - (await hasPermissionAsync(this.userId, 'view-l-room')) && agents?.includes(this.userId) && agents?.length === 1; - if (!hasAdminAccess && !hasAgentAccess) { - return API.v1.forbidden(); - } - - const createdAt = parseAndValidate('createdAt', createdAtParam); - const closedAt = parseAndValidate('closedAt', closedAtParam); - - return API.v1.success( - await LivechatVoip.findVoipRooms({ - agents, - open: open === 'true', - tags, - queue, - visitorId, - createdAt, - closedAt, - direction, - roomName, - options: { sort, offset, count, fields }, - }), - ); - }, - }, -); - -/** - * @openapi - * /voip/server/api/v1/voip/room.close - * post: - * description: Closes an open room - * based on rid and token. Setting rate limit for this too - * Because room creation happens 5/minute, rate limit for this api - * is also set to 5/minute. - * security: - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * rid: - * type: string - * token: - * type: string - * responses: - * 200: - * description: rid of closed room and a comment for closing room - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * rid: - * type: string - * comment: - * type: string - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'voip/room.close', - { authRequired: true, validateParams: isVoipRoomCloseProps, permissionsRequired: ['inbound-voip-calls'] }, - { - async post() { - const { rid, token, options } = this.bodyParams; - - const visitor = await LivechatVisitors.getVisitorByToken(token, {}); - if (!visitor) { - return API.v1.failure('invalid-token'); - } - const room = await LivechatVoip.findRoom(token, rid); - if (!room) { - return API.v1.failure('invalid-room'); - } - if (!room.open) { - return API.v1.failure('room-closed'); - } - const closeResult = await LivechatVoip.closeRoom(visitor, room, this.user, 'voip-call-wrapup', options); - if (!closeResult) { - return API.v1.failure(); - } - return API.v1.success({ rid }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/voip/server-connection.ts b/apps/meteor/app/api/server/v1/voip/server-connection.ts deleted file mode 100644 index 1bf2b85e8c7a4..0000000000000 --- a/apps/meteor/app/api/server/v1/voip/server-connection.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { VoipAsterisk } from '@rocket.chat/core-services'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../api'; - -API.v1.addRoute( - 'voip/managementServer/checkConnection', - { authRequired: true, permissionsRequired: ['manage-voip-contact-center-settings'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - host: String, - port: String, - username: String, - password: String, - }), - ); - const { host, port, username, password } = this.queryParams; - return API.v1.success(await VoipAsterisk.checkManagementConnection(host, port, username, password)); - }, - }, -); - -API.v1.addRoute( - 'voip/callServer/checkConnection', - { authRequired: true, permissionsRequired: ['manage-voip-contact-center-settings'] }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - websocketUrl: Match.Maybe(String), - host: Match.Maybe(String), - port: Match.Maybe(String), - path: Match.Maybe(String), - }), - ); - const { websocketUrl, host, port, path } = this.queryParams; - if (!websocketUrl && !(host && port && path)) { - return API.v1.failure('Incorrect / Insufficient Parameters'); - } - let socketUrl = websocketUrl as string; - if (!socketUrl) { - // We will assume that it is always secure. - // This is because you can not have webRTC working with non-secure server. - // It works on non-secure server if it is tested on localhost. - if (parseInt(port as string) !== 443) { - socketUrl = `wss://${host}:${port}/${(path as string).replace('/', '')}`; - } else { - socketUrl = `wss://${host}/${(path as string).replace('/', '')}`; - } - } - - return API.v1.success(await VoipAsterisk.checkCallserverConnection(socketUrl)); - }, - }, -); diff --git a/apps/meteor/app/apple/server/AppleCustomOAuth.ts b/apps/meteor/app/apple/server/AppleCustomOAuth.ts index 5ec0e8f570bc0..d6617b7b5e81d 100644 --- a/apps/meteor/app/apple/server/AppleCustomOAuth.ts +++ b/apps/meteor/app/apple/server/AppleCustomOAuth.ts @@ -5,7 +5,7 @@ import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { handleIdentityToken } from '../lib/handleIdentityToken'; export class AppleCustomOAuth extends CustomOAuth { - async getIdentity(_accessToken: string, query: Record): Promise { + override async getIdentity(_accessToken: string, query: Record): Promise { const { id_token: identityToken, user: userStr = '' } = query; let usrObj = {} as any; diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 0347d8e77f2fc..6952ac6f5457c 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -9,8 +9,8 @@ import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; import { registerGuest } from '@rocket.chat/omni-core'; -import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; +import { callbacks } from '../../../../server/lib/callbacks'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { setCustomFields } from '../../../livechat/server/lib/custom-fields'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 861f7ade14dca..ec0f6c8b3bc77 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,9 +1,9 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; -import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import type { GetMessagesOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; @@ -17,6 +17,41 @@ import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFrom import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; +const rawRoomProjection: FindOptions['projection'] = { + _id: 1, + fname: 1, + name: 1, + usernames: 1, + members: 1, + uids: 1, + default: 1, + ro: 1, + sysMes: 1, + msgs: 1, + ts: 1, + _updatedAt: 1, + closedAt: 1, + lm: 1, + description: 1, + customFields: 1, + prid: 1, + teamId: 1, + teamMain: 1, + livechatData: 1, + waitingResponse: 1, + open: 1, + source: 1, + closer: 1, + t: 1, + u: 1, + v: 1, + contactId: 1, + departmentId: 1, + closedBy: 1, + servedBy: 1, + responseBy: 1, +}; + export class AppRoomBridge extends RoomBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); @@ -151,6 +186,37 @@ export class AppRoomBridge extends RoomBridge { return promises as Promise; } + protected async getAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise> { + this.orch.debugLog(`The App ${appId} is getting all rooms with options`, options); + + const { limit = 100, skip = 0 } = options; + + const findOptions: FindOptions = { + sort: { ts: -1 }, + skip, + limit: Math.min(limit, 100), + projection: rawRoomProjection, + }; + + const { types, discussions, teams } = filters; + + const rooms: IRoomRaw[] = []; + + const roomConverter = this.orch.getConverters()?.get('rooms'); + if (!roomConverter) { + throw new Error('Room converter not found'); + } + + for await (const room of Rooms.findAllByTypesAndDiscussionAndTeam({ types, discussions, teams }, findOptions)) { + const converted = await roomConverter.convertRoomRaw(room); + if (converted) { + rooms.push(converted); + } + } + + return rooms; + } + protected async getDirectByUsernames(usernames: Array, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting direct room by usernames: "${usernames}"`); const room = await Rooms.findDirectRoomContainingAllUsernames(usernames, {}); diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 4152b12bbe57c..c81792a0bb390 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,6 +20,106 @@ export class AppRoomsConverter { return this.convertRoom(room); } + convertRoomRaw(room) { + if (!room) { + return undefined; + } + + const mapUserLookup = (user) => + user && { + _id: user._id ?? user.id, + ...(user.username && { username: user.username }), + ...(user.name && { name: user.name }), + }; + + const map = { + id: '_id', + displayName: 'fname', + slugifiedName: 'name', + members: 'members', + userIds: 'uids', + usernames: 'usernames', + messageCount: 'msgs', + createdAt: 'ts', + updatedAt: '_updatedAt', + closedAt: 'closedAt', + lastModifiedAt: 'lm', + customFields: 'customFields', + livechatData: 'livechatData', + isWaitingResponse: 'waitingResponse', + isOpen: 'open', + description: 'description', + source: 'source', + closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', + isDefault: 'default', + isReadOnly: 'ro', + contactId: 'contactId', + departmentId: 'departmentId', + parentRoomId: 'prid', + visitor: (data) => { + const { v } = data; + if (!v) { + return undefined; + } + + delete data.v; + + const { _id: id, ...rest } = v; + + return { + id, + ...rest, + }; + }, + displaySystemMessages: (data) => { + const { sysMes } = data; + delete data.sysMes; + return typeof sysMes === 'undefined' ? true : sysMes; + }, + type: (data) => { + const result = this._convertTypeToApp(data.t); + delete data.t; + return result; + }, + creator: (data) => { + if (!data.u) { + return undefined; + } + const creator = mapUserLookup(data.u); + delete data.u; + return creator; + }, + closedBy: (data) => { + if (!data.closedBy) { + return undefined; + } + const { closedBy } = data; + delete data.closedBy; + return mapUserLookup(closedBy); + }, + servedBy: (data) => { + if (!data.servedBy) { + return undefined; + } + const { servedBy } = data; + delete data.servedBy; + return mapUserLookup(servedBy); + }, + responseBy: (data) => { + if (!data.responseBy) { + return undefined; + } + const { responseBy } = data; + delete data.responseBy; + return mapUserLookup(responseBy); + }, + }; + + return transformMappedData(room, map); + } + async __getCreator(user) { if (!user) { return; @@ -54,6 +154,7 @@ export class AppRoomsConverter { username: visitor.username, token: visitor.token, status: visitor.status || 'online', + activity: visitor.activity, ...(lastMessageTs && { lastMessageTs }), ...(phone && { phone }), }; @@ -200,6 +301,8 @@ export class AppRoomsConverter { description: 'description', source: 'source', closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', isDefault: (room) => { const result = !!room.default; delete room.default; diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index c8fb0b7c4a21c..00b8b3888ae74 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -36,6 +36,7 @@ export class AppVisitorsConverter { visitorEmails: 'visitorEmails', livechatData: 'livechatData', status: 'status', + activity: 'activity', }; return transformMappedData(visitor, map); diff --git a/apps/meteor/app/authentication/server/hooks/login.ts b/apps/meteor/app/authentication/server/hooks/login.ts index 10b91271c88fa..191639e208dee 100644 --- a/apps/meteor/app/authentication/server/hooks/login.ts +++ b/apps/meteor/app/authentication/server/hooks/login.ts @@ -1,6 +1,6 @@ import { Accounts } from 'meteor/accounts-base'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import type { ILoginAttempt } from '../ILoginAttempt'; import { logFailedLoginAttempts } from '../lib/logLoginAttempts'; diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index a56f237c6e527..12825cde0f678 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -8,10 +8,10 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { callbacks } from '../../../../lib/callbacks'; -import { beforeCreateUserCallback } from '../../../../lib/callbacks/beforeCreateUserCallback'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { beforeCreateUserCallback } from '../../../../server/lib/callbacks/beforeCreateUserCallback'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { getMaxLoginTokens } from '../../../../server/lib/getMaxLoginTokens'; import { i18n } from '../../../../server/lib/i18n'; @@ -204,7 +204,9 @@ const onCreateUserAsync = async function (options, user = {}) { } user.status = 'offline'; + user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); + user.inactiveReason = settings.get('Accounts_ManuallyApproveNewUsers') && !user.active ? 'pending_approval' : undefined; if (!user.name) { if (options.profile) { @@ -292,7 +294,7 @@ Accounts.insertUserDoc = async function (options, user) { delete user.globalRoles; - if (user.services && !user.services.password) { + if (user.services && !user.services.password && !options.skipAuthServiceDefaultRoles) { const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); if (defaultAuthServiceRoles.length > 0) { diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index 49855eb7d32f1..600bf15793cc9 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -156,10 +156,6 @@ export const permissions = [ _id: 'add-livechat-department-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'], }, - { - _id: 'view-livechat-current-chats', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, { _id: 'view-livechat-real-time-monitoring', roles: ['livechat-manager', 'livechat-monitor', 'admin'], @@ -211,23 +207,6 @@ export const permissions = [ { _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] }, { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, - // VOIP Permissions - // allows to manage voip calls configuration - { _id: 'manage-voip-call-settings', roles: ['livechat-manager', 'admin'] }, - { _id: 'manage-voip-contact-center-settings', roles: ['livechat-manager', 'admin'] }, - // allows agent-extension association. - { _id: 'manage-agent-extension-association', roles: ['admin'] }, - { _id: 'view-agent-extension-association', roles: ['livechat-manager', 'admin', 'livechat-agent'] }, - // allows to receive a voip call - { _id: 'inbound-voip-calls', roles: ['livechat-agent'] }, - - // Allow managing team collab voip extensions - { _id: 'manage-voip-extensions', roles: ['admin'] }, - // Allow viewing the extension number of other users - { _id: 'view-user-voip-extension', roles: ['admin', 'user'] }, - // Allow viewing details of an extension - { _id: 'view-voip-extension-details', roles: ['admin', 'user'] }, - // New Media calls permissions { _id: 'allow-internal-voice-calls', roles: ['admin', 'user'] }, { _id: 'allow-external-voice-calls', roles: ['admin', 'user'] }, diff --git a/apps/meteor/app/authorization/server/index.ts b/apps/meteor/app/authorization/server/index.ts index a91e23610230a..c74062e774b41 100644 --- a/apps/meteor/app/authorization/server/index.ts +++ b/apps/meteor/app/authorization/server/index.ts @@ -4,9 +4,7 @@ import { getUsersInRole } from './functions/getUsersInRole'; import { subscriptionHasRole } from './functions/hasRole'; import './methods/addPermissionToRole'; import './methods/addUserToRole'; -import './methods/deleteRole'; import './methods/removeRoleFromPermission'; -import './methods/removeUserFromRole'; import './streamer/permissions'; export { getRoles, getUsersInRole, subscriptionHasRole, canAccessRoomAsync, roomAccessAttributes }; diff --git a/apps/meteor/app/authorization/server/methods/addUserToRole.ts b/apps/meteor/app/authorization/server/methods/addUserToRole.ts index 461c0a11418ad..77bdae79da3bc 100644 --- a/apps/meteor/app/authorization/server/methods/addUserToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addUserToRole.ts @@ -1,21 +1,12 @@ import { api } from '@rocket.chat/core-services'; import type { IRole, IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Roles, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { hasPermissionAsync } from '../functions/hasPermission'; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope: string | undefined): Promise; - } -} - export const addUserToRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise => { if (!(await hasPermissionAsync(userId, 'access-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { @@ -30,21 +21,12 @@ export const addUserToRole = async (userId: string, roleId: string, username: IU }); } - let role = await Roles.findOneById>(roleId, { projection: { _id: 1 } }); - if (!role) { - role = await Roles.findOneByName>(roleId, { projection: { _id: 1 } }); + const role = await Roles.findOneById>(roleId, { projection: { _id: 1 } }); - if (!role) { - throw new Meteor.Error('error-invalid-role', 'Invalid Role', { - method: 'authorization:addUserToRole', - }); - } - methodDeprecationLogger.deprecatedParameterUsage( - 'authorization:addUserToRole', - 'role', - '7.0.0', - ({ parameter, method, version }) => `Calling ${method} with \`${parameter}\` names is deprecated and will be removed ${version}`, - ); + if (!role) { + throw new Meteor.Error('error-invalid-role', 'Invalid Role', { + method: 'authorization:addUserToRole', + }); } if (role._id === 'admin' && !(await hasPermissionAsync(userId, 'assign-admin-role'))) { @@ -89,20 +71,3 @@ export const addUserToRole = async (userId: string, roleId: string, username: IU return add; }; - -Meteor.methods({ - async 'authorization:addUserToRole'(roleId: IRole['_id'], username: IUser['username'], scope) { - methodDeprecationLogger.method('authorization:addUserToRole', '8.0.0', '/v1/roles.addUserToRole'); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { - method: 'authorization:addUserToRole', - action: 'Accessing_permissions', - }); - } - - return addUserToRole(userId, roleId, username, scope); - }, -}); diff --git a/apps/meteor/app/authorization/server/methods/deleteRole.ts b/apps/meteor/app/authorization/server/methods/deleteRole.ts deleted file mode 100644 index c327d1f1a2bc3..0000000000000 --- a/apps/meteor/app/authorization/server/methods/deleteRole.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { IRole } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Roles } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; -import type { DeleteResult } from 'mongodb'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { hasPermissionAsync } from '../functions/hasPermission'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'authorization:deleteRole'(roleId: IRole['_id'] | IRole['name']): Promise; - } -} - -Meteor.methods({ - async 'authorization:deleteRole'(roleId) { - methodDeprecationLogger.method('authorization:deleteRole', '8.0.0', '/v1/roles.delete'); - - const userId = Meteor.userId(); - - if (!userId || !(await hasPermissionAsync(userId, 'access-permissions'))) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { - method: 'authorization:deleteRole', - action: 'Accessing_permissions', - }); - } - - const options = { - projection: { - _id: 1, - protected: 1, - }, - }; - - let role = await Roles.findOneById>(roleId, options); - if (!role) { - role = await Roles.findOneByName>(roleId, options); - - if (!role) { - throw new Meteor.Error('error-invalid-role', 'Invalid role', { - method: 'authorization:deleteRole', - }); - } - - methodDeprecationLogger.deprecatedParameterUsage( - 'authorization:deleteRole', - 'role', - '7.0.0', - ({ parameter, method, version }) => `Calling ${method} with ${parameter} names is deprecated and will be removed ${version}`, - ); - } - - if (role.protected) { - throw new Meteor.Error('error-delete-protected-role', 'Cannot delete a protected role', { - method: 'authorization:deleteRole', - }); - } - - const users = await Roles.countUsersInRole(role._id); - - if (users > 0) { - throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use", { - method: 'authorization:deleteRole', - }); - } - - return Roles.removeById(role._id); - }, -}); diff --git a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts index 5efcc71b3577b..6caef9c656597 100644 --- a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts +++ b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts @@ -1,21 +1,12 @@ import { api } from '@rocket.chat/core-services'; import type { IRole, IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Roles, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { hasPermissionAsync } from '../functions/hasPermission'; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'authorization:removeUserFromRole'(roleId: IRole['_id'], username: IUser['username'], scope?: string): Promise; - } -} - export const removeUserFromRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise => { if (!(await hasPermissionAsync(userId, 'access-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', { @@ -30,21 +21,11 @@ export const removeUserFromRole = async (userId: string, roleId: string, usernam }); } - let role = await Roles.findOneById>(roleId, { projection: { _id: 1 } }); + const role = await Roles.findOneById>(roleId, { projection: { _id: 1 } }); if (!role) { - role = await Roles.findOneByName>(roleId, { projection: { _id: 1 } }); - if (!role) { - throw new Meteor.Error('error-invalid-role', 'Invalid Role', { - method: 'authorization:removeUserFromRole', - }); - } - - methodDeprecationLogger.deprecatedParameterUsage( - 'authorization:removeUserFromRole', - 'role', - '7.0.0', - ({ parameter, method, version }) => `Calling ${method} with ${parameter} names is deprecated and will be removed ${version}`, - ); + throw new Meteor.Error('error-invalid-role', 'Invalid Role', { + method: 'authorization:removeUserFromRole', + }); } const user = await Users.findOneByUsernameIgnoringCase(username, { @@ -94,20 +75,3 @@ export const removeUserFromRole = async (userId: string, roleId: string, usernam return remove; }; - -Meteor.methods({ - async 'authorization:removeUserFromRole'(roleId, username, scope) { - methodDeprecationLogger.method('authorization:removeUserFromRole', '8.0.0', '/v1/roles.removeUserFromRole'); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', { - method: 'authorization:removeUserFromRole', - action: 'Accessing_permissions', - }); - } - - return removeUserFromRole(userId, roleId, username, scope); - }, -}); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 23e1b189a7920..afb4b5d5cf5fd 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -13,8 +13,8 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { callbacks } from '../../../lib/callbacks'; import { isTruthy } from '../../../lib/isTruthy'; +import { callbacks } from '../../../server/lib/callbacks'; import { notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; import { Markdown } from '../../markdown/server'; import { settings } from '../../settings/server'; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index c2fd6a95958f3..61ad721dc0593 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -5,7 +5,7 @@ import { Integrations, Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import type { Document, UpdateResult } from 'mongodb'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; import { notifyOnIntegrationChangedByChannels, notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts index 925b4833efd90..f916259c03364 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts @@ -4,7 +4,7 @@ import { Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; export const saveRoomTopic = async ( rid: string, diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 7cbdca852cd6d..6ca88c1fdabc5 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -1,5 +1,5 @@ import { Team } from '@rocket.chat/core-services'; -import type { IRoom, IRoomWithRetentionPolicy, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; +import type { IRoom, IRoomWithRetentionPolicy, IUser, MessageTypesValues, ITeam } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Users } from '@rocket.chat/models'; @@ -11,6 +11,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; import { saveReactWhenReadOnly } from '../functions/saveReactWhenReadOnly'; import { saveRoomAnnouncement } from '../functions/saveRoomAnnouncement'; import { saveRoomCustomFields } from '../functions/saveRoomCustomFields'; @@ -61,14 +62,33 @@ type RoomSettingsValidators = { const hasRetentionPolicy = (room: IRoom & { retention?: any }): room is IRoomWithRetentionPolicy => 'retention' in room && room.retention !== undefined; +const isAbacManagedRoom = (room: IRoom): boolean => { + return room.t === 'p' && settings.get('ABAC_Enabled') && Array.isArray(room?.abacAttributes) && room.abacAttributes.length > 0; +}; + +const isAbacManagedTeam = (team: Partial | null, teamRoom: IRoom): boolean => { + return ( + team?.type === TEAM_TYPE.PRIVATE && + settings.get('ABAC_Enabled') && + Array.isArray(teamRoom?.abacAttributes) && + teamRoom.abacAttributes.length > 0 + ); +}; + const validators: RoomSettingsValidators = { - async default({ userId }) { + async default({ userId, room, value }) { if (!(await hasPermissionAsync(userId, 'view-room-administration'))) { throw new Meteor.Error('error-action-not-allowed', 'Viewing room administration is not allowed', { method: 'saveRoomSettings', action: 'Viewing_room_administration', }); } + if (isAbacManagedRoom(room) && value) { + throw new Meteor.Error('error-action-not-allowed', 'Setting an ABAC managed room as default is not allowed', { + method: 'saveRoomSettings', + action: 'Viewing_room_administration', + }); + } }, async featured({ userId }) { if (!(await hasPermissionAsync(userId, 'view-room-administration'))) { @@ -98,6 +118,13 @@ const validators: RoomSettingsValidators = { }); } + if (isAbacManagedRoom(room) && value !== 'p') { + throw new Meteor.Error('error-action-not-allowed', 'Changing an ABAC managed private room to public is not allowed', { + method: 'saveRoomSettings', + action: 'Change_Room_Type', + }); + } + if (!room.teamId) { return; } @@ -116,6 +143,13 @@ const validators: RoomSettingsValidators = { action: 'Change_Room_Type', }); } + + if (isAbacManagedTeam(team, room) && value !== 'p') { + throw new Meteor.Error('error-action-not-allowed', 'Changing an ABAC managed private team room to public is not allowed', { + method: 'saveRoomSettings', + action: 'Change_Room_Type', + }); + } }, async encrypted({ userId, value, room, rid }) { if (value !== room.encrypted) { diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 0ee4ba09ff57a..08b551782f6b8 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -3,13 +3,13 @@ import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { z } from 'zod'; -import { callbacks } from '../../../../lib/callbacks'; +import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceLicenseError } from '../../../../lib/errors/CloudWorkspaceLicenseError'; +import { callbacks } from '../../../../server/lib/callbacks'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { LICENSE_VERSION } from '../license'; -import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; const workspaceLicensePayloadSchema = z.object({ version: z.number(), diff --git a/apps/meteor/app/cloud/server/functions/removeLicense.ts b/apps/meteor/app/cloud/server/functions/removeLicense.ts index 88f1d68251777..31cd23df14558 100644 --- a/apps/meteor/app/cloud/server/functions/removeLicense.ts +++ b/apps/meteor/app/cloud/server/functions/removeLicense.ts @@ -3,9 +3,9 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; -import { callbacks } from '../../../../lib/callbacks'; import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; export async function removeLicense() { diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts index f28821b083875..572a9a4a1bbef 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts @@ -1,14 +1,14 @@ import { DuplicatedLicenseError } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; -import { callbacks } from '../../../../../lib/callbacks'; +import { fetchWorkspaceSyncPayload } from './fetchWorkspaceSyncPayload'; import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { SystemLogger } from '../../../../../server/lib/logger/system'; import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; -import { fetchWorkspaceSyncPayload } from './fetchWorkspaceSyncPayload'; export async function syncCloudData() { try { diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index bfed645e146de..4a5f6779974e5 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -10,8 +10,8 @@ import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; -import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; +import { callbacks } from '../../../server/lib/callbacks'; import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { settings } from '../../settings/server'; diff --git a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts index 86b2b30b699df..494c84590b50c 100644 --- a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts +++ b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Messages, Rooms, VideoConference } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; diff --git a/apps/meteor/app/dolphin/server/lib.ts b/apps/meteor/app/dolphin/server/lib.ts index 65a8ca6d2dacf..826a277b81e4e 100644 --- a/apps/meteor/app/dolphin/server/lib.ts +++ b/apps/meteor/app/dolphin/server/lib.ts @@ -2,8 +2,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { callbacks } from '../../../lib/callbacks'; -import { beforeCreateUserCallback } from '../../../lib/callbacks/beforeCreateUserCallback'; +import { callbacks } from '../../../server/lib/callbacks'; +import { beforeCreateUserCallback } from '../../../server/lib/callbacks/beforeCreateUserCallback'; import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { settings } from '../../settings/server'; diff --git a/apps/meteor/app/e2e/server/beforeCreateRoom.ts b/apps/meteor/app/e2e/server/beforeCreateRoom.ts index 19a8bcb2364ea..cb04d0def049d 100644 --- a/apps/meteor/app/e2e/server/beforeCreateRoom.ts +++ b/apps/meteor/app/e2e/server/beforeCreateRoom.ts @@ -1,4 +1,4 @@ -import { prepareCreateRoomCallback } from '../../../lib/callbacks/beforeCreateRoomCallback'; +import { prepareCreateRoomCallback } from '../../../server/lib/callbacks/beforeCreateRoomCallback'; import { settings } from '../../settings/server'; prepareCreateRoomCallback.add(({ type, extraData }) => { diff --git a/apps/meteor/app/e2e/server/index.ts b/apps/meteor/app/e2e/server/index.ts index b0d9dadcd2e2f..8c8c59488fd79 100644 --- a/apps/meteor/app/e2e/server/index.ts +++ b/apps/meteor/app/e2e/server/index.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import './beforeCreateRoom'; import './methods/setUserPublicAndPrivateKeys'; diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index dd74041a90b71..1c018cb71cd37 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -1,21 +1,11 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChanged, notifyOnRoomChangedById, } from '../../../lib/server/lib/notifyListener'; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'e2e.updateGroupKey'(rid: string, uid: string, key: string): Promise; - } -} - export async function updateGroupKey(rid: string, uid: string, key: string, callerUserId: string) { // I have a subscription to this room const mySub = await Subscriptions.findOneByRoomIdAndUserId(rid, callerUserId); @@ -46,16 +36,3 @@ export async function updateGroupKey(rid: string, uid: string, key: string, call } } } - -Meteor.methods({ - async 'e2e.updateGroupKey'(rid, uid, key) { - methodDeprecationLogger.method('e2e.updateGroupKey', '8.0.0', '/v1/e2e.acceptSuggestedGroupKey'); - - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' }); - } - - return updateGroupKey(rid, uid, key, userId); - }, -}); diff --git a/apps/meteor/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts deleted file mode 100644 index 47728667b9071..0000000000000 --- a/apps/meteor/app/ecdh/Session.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { X25519SecretKey, CryptographyKey } from 'sodium-plus'; -import { SodiumPlus, X25519PublicKey } from 'sodium-plus'; - -export class Session { - // Encoding for the key exchange, no requirements to be small - protected readonly stringFormatKey: BufferEncoding = 'base64'; - - // Encoding for the transfer of encrypted data, should be smaller as possible - protected readonly stringFormatEncryptedData: BufferEncoding = 'base64'; - - // Encoding before the encryption to keep unicode chars - protected readonly stringFormatRawData: BufferEncoding = 'base64'; - - protected decryptKey: CryptographyKey; - - protected encryptKey: CryptographyKey; - - protected secretKey: X25519SecretKey; - - public publicKey: X25519PublicKey; - - private static sodium: SodiumPlus | undefined; - - async sodium(): Promise { - if (!Session.sodium) { - Session.sodium = await SodiumPlus.auto(); - } - - return Session.sodium; - } - - get publicKeyString(): string { - return this.publicKey.toString(this.stringFormatKey); - } - - publicKeyFromString(text: string): X25519PublicKey { - return new X25519PublicKey(Buffer.from(text, this.stringFormatKey)); - } - - async encryptToBuffer(plaintext: string | Buffer): Promise { - const sodium = await this.sodium(); - const nonce = await sodium.randombytes_buf(24); - - const buffer = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext); - - const ciphertext = await sodium.crypto_secretbox(Buffer.from(buffer).toString(this.stringFormatRawData), nonce, this.encryptKey); - - return Buffer.concat([nonce, ciphertext]); - } - - async encrypt(plaintext: string | Buffer): Promise { - const buffer = await this.encryptToBuffer(plaintext); - return buffer.toString(this.stringFormatEncryptedData); - } - - async decryptToBuffer(data: string | Buffer): Promise { - const sodium = await this.sodium(); - const buffer = Buffer.from(Buffer.isBuffer(data) ? data.toString() : data, this.stringFormatEncryptedData); - - const decrypted = await sodium.crypto_secretbox_open(buffer.slice(24), buffer.slice(0, 24), this.decryptKey); - - return Buffer.from(decrypted.toString(), this.stringFormatRawData); - } - - async decrypt(data: string | Buffer): Promise { - const buffer = await this.decryptToBuffer(data); - return buffer.toString(); - } -} diff --git a/apps/meteor/app/ecdh/client/ClientSession.ts b/apps/meteor/app/ecdh/client/ClientSession.ts deleted file mode 100644 index 6e87cbae6b139..0000000000000 --- a/apps/meteor/app/ecdh/client/ClientSession.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Session } from '../Session'; - -export class ClientSession extends Session { - async init(): Promise { - const sodium = await this.sodium(); - - const clientKeypair = await sodium.crypto_box_keypair(); - this.secretKey = await sodium.crypto_box_secretkey(clientKeypair); - this.publicKey = await sodium.crypto_box_publickey(clientKeypair); - - return this.publicKey.toString(this.stringFormatKey); - } - - async setServerKey(serverPublic: string): Promise { - const sodium = await this.sodium(); - - const [decryptKey, encryptKey] = await sodium.crypto_kx_client_session_keys( - this.publicKey, - this.secretKey, - this.publicKeyFromString(serverPublic), - ); - - this.decryptKey = decryptKey; - this.encryptKey = encryptKey; - } -} diff --git a/apps/meteor/app/emoji-emojione/server/callbacks.ts b/apps/meteor/app/emoji-emojione/server/callbacks.ts index 5f73b73d9d4b9..081e115a152a5 100644 --- a/apps/meteor/app/emoji-emojione/server/callbacks.ts +++ b/apps/meteor/app/emoji-emojione/server/callbacks.ts @@ -1,7 +1,7 @@ import emojione from 'emojione'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; Meteor.startup(() => { callbacks.add( diff --git a/apps/meteor/app/file-upload/server/config/GridFS.ts b/apps/meteor/app/file-upload/server/config/GridFS.ts index 551569d4c58c4..bb9dfa5891f26 100644 --- a/apps/meteor/app/file-upload/server/config/GridFS.ts +++ b/apps/meteor/app/file-upload/server/config/GridFS.ts @@ -28,7 +28,7 @@ class ExtractRange extends stream.Transform { this.bytes_read = 0; } - _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) { + override _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) { if (this.bytes_read > this.stop) { // done reading this.end(); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 8ed534c7015b9..dd67e3489f5da 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -12,9 +12,9 @@ import { Rooms, Uploads, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; +import { callbacks } from '../../../../server/lib/callbacks'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index 4f5eeba7d88b9..cab23a46da62a 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -19,7 +19,7 @@ export class CsvImporter extends Importer { this.csvParser = parse; } - async prepareUsingLocalFile(fullFilePath: string): Promise { + override async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); diff --git a/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts b/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts index ca5f66bc05ea5..5415f10e63887 100644 --- a/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts +++ b/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts @@ -18,7 +18,7 @@ export class ContactImporter extends Importer { this.csvParser = parse; } - async prepareUsingLocalFile(fullFilePath: string): Promise { + override async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); diff --git a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts index f057da4a625d4..746bb41681336 100644 --- a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts +++ b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts @@ -33,7 +33,7 @@ export class PendingAvatarImporter extends Importer { return fileCount; } - async startImport(importSelection: IImporterShortSelection): Promise { + override async startImport(importSelection: IImporterShortSelection): Promise { const pendingFileUserList = Users.findAllUsersWithPendingAvatar(); try { for await (const user of pendingFileUserList) { diff --git a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts index a6d147cf8df35..818031d07916e 100644 --- a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts +++ b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts @@ -43,7 +43,7 @@ export class PendingFileImporter extends Importer { return fileCount; } - async startImport(importSelection: IImporterShortSelection): Promise { + override async startImport(importSelection: IImporterShortSelection): Promise { const downloadedFileIds: string[] = []; const maxFileCount = 10; const maxFileSize = 1024 * 1024 * 500; diff --git a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index c05c6da8d690e..aa51f56daa47f 100644 --- a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -20,7 +20,7 @@ export class SlackUsersImporter extends Importer { this.csvParser = parse; } - async prepareUsingLocalFile(fullFilePath: string): Promise { + override async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index c7e1b7c624ce7..f0fefed3f749e 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -267,7 +267,7 @@ export class SlackImporter extends Importer { return data.length; } - async prepareUsingLocalFile(fullFilePath: string): Promise { + override async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); diff --git a/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts b/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts index eb6b25acd3851..01067cbe118f8 100644 --- a/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts @@ -16,7 +16,7 @@ export class ContactConverter extends RecordConverter { return validateCustomFields(allowedCustomFields, customFields, { ignoreAdditionalFields: true }); } - protected async convertRecord(record: IImportContactRecord): Promise { + protected override async convertRecord(record: IImportContactRecord): Promise { const { data } = record; await createContact({ @@ -36,7 +36,7 @@ export class ContactConverter extends RecordConverter { return LivechatVisitors.getNextVisitorUsername(); } - protected getDataType(): 'contact' { + protected override getDataType(): 'contact' { return 'contact'; } } diff --git a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts index 732e46b4398bc..aa47be4434186 100644 --- a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts @@ -27,7 +27,7 @@ type IMessageReactions = Record; export class MessageConverter extends RecordConverter { private rids: string[] = []; - async convertData({ afterImportAllMessagesFn, ...callbacks }: MessageConversionCallbacks = {}): Promise { + override async convertData({ afterImportAllMessagesFn, ...callbacks }: MessageConversionCallbacks = {}): Promise { this.rids = []; await super.convertData(callbacks); @@ -76,7 +76,7 @@ export class MessageConverter extends RecordConverter { } } - protected async convertRecord(record: IImportMessageRecord): Promise { + protected override async convertRecord(record: IImportMessageRecord): Promise { await this.insertMessage(record.data); return true; } @@ -257,7 +257,7 @@ export class MessageConverter extends RecordConverter { } } - protected getDataType(): 'message' { + protected override getDataType(): 'message' { return 'message'; } } diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts index ba7d01b0009ee..a19d5f0ae0fd3 100644 --- a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -20,7 +20,7 @@ export class RoomConverter extends RecordConverter { return this.convertData(callbacks); } - protected async convertRecord(record: IImportChannelRecord): Promise { + protected override async convertRecord(record: IImportChannelRecord): Promise { const { data } = record; if (!data.name && data.t !== 'd') { @@ -193,7 +193,7 @@ export class RoomConverter extends RecordConverter { throw new Error('importer-channel-invalid-creator'); } - protected getDataType(): 'channel' { + protected override getDataType(): 'channel' { return 'channel'; } } diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index dbfbd9f0c94d9..efa721a518ddd 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -7,7 +7,7 @@ import { Accounts } from 'meteor/accounts-base'; import { RecordConverter, type RecordConverterOptions } from './RecordConverter'; import { generateTempPassword } from './generateTempPassword'; -import { callbacks as systemCallbacks } from '../../../../../lib/callbacks'; +import { callbacks as systemCallbacks } from '../../../../../server/lib/callbacks'; import { addUserToDefaultChannels } from '../../../../lib/server/functions/addUserToDefaultChannels'; import { generateUsernameSuggestion } from '../../../../lib/server/functions/getUsernameSuggestion'; import { saveUserIdentity } from '../../../../lib/server/functions/saveUserIdentity'; @@ -32,7 +32,7 @@ export class UserConverter extends RecordConverter(); - protected async convertRecord(record: IImportUserRecord): Promise { + protected override async convertRecord(record: IImportUserRecord): Promise { const { data, _id } = record; data.importIds = data.importIds.filter((item) => item); @@ -55,7 +55,7 @@ export class UserConverter extends RecordConverter { + override async convertData(userCallbacks: IConversionCallbacks = {}): Promise { this.insertedIds.clear(); this.updatedIds.clear(); @@ -424,7 +424,7 @@ export class UserConverter extends RecordConverter u.toUpperCase()); } - protected getDataType(): 'user' { + protected override getDataType(): 'user' { return 'user'; } } diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 59770587170a4..a9b77aadf65bb 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -9,10 +9,11 @@ import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit'; import { WebApp } from 'meteor/webapp'; import _ from 'underscore'; +import { isPlainObject } from '../../../../lib/utils/isPlainObject'; import { APIClass } from '../../../api/server/ApiClass'; import type { RateLimiterOptions } from '../../../api/server/api'; import { API, defaultRateLimiterOptions } from '../../../api/server/api'; -import type { FailureResult, PartialThis, SuccessResult, UnavailableResult } from '../../../api/server/definition'; +import type { FailureResult, GenericRouteExecutionContext, SuccessResult, UnavailableResult } from '../../../api/server/definition'; import type { WebhookResponseItem } from '../../../lib/server/functions/processWebhookMessage'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { settings } from '../../../settings/server'; @@ -39,11 +40,10 @@ type IntegrationOptions = { }; }; -type IntegrationThis = Omit & { +type IntegrationThis = GenericRouteExecutionContext & { request: Request & { integration: IIncomingIntegration; }; - urlParams: Record; user: IUser & { username: RequiredField }; }; @@ -138,8 +138,8 @@ async function executeIntegrationRest( const scriptEngine = getEngine(this.request.integration); - let { bodyParams } = this; - const separateResponse = this.bodyParams?.separateResponse === true; + let bodyParams = isPlainObject(this.bodyParams) ? this.bodyParams : {}; + const separateResponse = bodyParams.separateResponse === true; let scriptResponse: Record | undefined; if (scriptEngine.integrationHasValidScript(this.request.integration) && this.request.body) { @@ -152,21 +152,22 @@ async function executeIntegrationRest( const contentRaw = Buffer.concat(buffers).toString('utf8'); const protocol = `${this.request.headers.get('x-forwarded-proto')}:` || 'http:'; const url = new URL(this.request.url, `${protocol}//${this.request.headers.get('host')}`); + const query = isPlainObject(this.queryParams) ? this.queryParams : {}; const request = { url: { + query, hash: url.hash, search: url.search, - query: this.queryParams, pathname: url.pathname, path: this.request.url, }, url_raw: this.request.url, url_params: this.urlParams, - content: this.bodyParams, + content: bodyParams, content_raw: contentRaw, headers: Object.fromEntries(this.request.headers.entries()), - body: this.bodyParams, + body: bodyParams, user: { _id: this.user._id, name: this.user.name || '', @@ -187,11 +188,11 @@ async function executeIntegrationRest( }); return API.v1.success(); } - if (result && result.error) { + if (result?.error) { return API.v1.failure(result.error); } - bodyParams = result && result.content; + bodyParams = result?.content; if (!('separateResponse' in bodyParams)) { bodyParams.separateResponse = separateResponse; @@ -312,8 +313,8 @@ function integrationInfoRest(): { statusCode: number; body: { success: boolean } } class WebHookAPI extends APIClass<'/hooks'> { - async authenticatedRoute(this: IntegrationThis): Promise { - const { integrationId, token } = this.urlParams; + override async authenticatedRoute(routeContext: IntegrationThis): Promise { + const { integrationId, token } = routeContext.urlParams; const integration = await Integrations.findOneByIdAndToken(integrationId, decodeURIComponent(token)); if (!integration) { @@ -322,12 +323,12 @@ class WebHookAPI extends APIClass<'/hooks'> { throw new Error('Invalid integration id or token provided.'); } - this.request.integration = integration; + routeContext.request.integration = integration; - return Users.findOneById(this.request.integration.userId); + return Users.findOneById(routeContext.request.integration.userId); } - shouldAddRateLimitToRoute(options: { rateLimiterOptions?: RateLimiterOptions | boolean }): boolean { + override shouldAddRateLimitToRoute(options: { rateLimiterOptions?: RateLimiterOptions | boolean }): boolean { const { rateLimiterOptions } = options; return ( (typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) && @@ -336,14 +337,14 @@ class WebHookAPI extends APIClass<'/hooks'> { ); } - async shouldVerifyRateLimit(): Promise { + override async shouldVerifyRateLimit(): Promise { return ( settings.get('API_Enable_Rate_Limiter') === true && (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true) ); } - async enforceRateLimit( + override async enforceRateLimit( objectForRateLimitMatch: RateLimiterOptionsToCheck, request: Request, response: Response, diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.ts b/apps/meteor/app/integrations/server/lib/triggerHandler.ts index 9f4519e5b01e5..e3808da9ef8e9 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.ts +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.ts @@ -216,12 +216,17 @@ class RocketChatIntegrationHandler { } break; case 'roomJoined': - case 'roomLeft': if (args.length >= 3) { argObject.user = args[1] as IUser; argObject.room = args[2] as IRoom; } break; + case 'roomLeft': + if (args.length >= 3) { + argObject.user = (args[1] as { user: IUser })?.user; + argObject.room = args[2] as IRoom; + } + break; case 'userCreated': if (args.length >= 2) { argObject.user = args[1] as IUser; @@ -235,7 +240,9 @@ class RocketChatIntegrationHandler { outgoingLogger.debug({ msg: `Got the event arguments for the event: ${argObject.event}`, - argObject, + messageId: argObject.message?._id, + roomId: argObject.room?._id, + userId: argObject.user?._id || argObject.owner?._id, }); return argObject; diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts index 06fc9b0a9e1af..adf3d5b8bdf4d 100644 --- a/apps/meteor/app/integrations/server/triggers.ts +++ b/apps/meteor/app/integrations/server/triggers.ts @@ -1,6 +1,6 @@ import { triggerHandler } from './lib/triggerHandler'; -import { callbacks } from '../../../lib/callbacks'; -import { afterLeaveRoomCallback } from '../../../lib/callbacks/afterLeaveRoomCallback'; +import { callbacks } from '../../../server/lib/callbacks'; +import { afterLeaveRoomCallback } from '../../../server/lib/callbacks/afterLeaveRoomCallback'; const callbackHandler = function _callbackHandler(eventType: string) { return function _wrapperFunction(...args: any[]) { diff --git a/apps/meteor/app/invites/server/functions/findOrCreateInvite.ts b/apps/meteor/app/invites/server/functions/findOrCreateInvite.ts index 052445a1ebc99..fa905cc345c6d 100644 --- a/apps/meteor/app/invites/server/functions/findOrCreateInvite.ts +++ b/apps/meteor/app/invites/server/functions/findOrCreateInvite.ts @@ -63,6 +63,13 @@ export const findOrCreateInvite = async (userId: string, invite: Pick { if (!token || typeof token !== 'string') { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { @@ -25,6 +27,13 @@ export const validateInviteToken = async (token: string) => { }); } + if (settings.get('ABAC_Enabled') && room?.abacAttributes?.length) { + throw new Meteor.Error('error-invalid-room', 'Room is ABAC managed', { + method: 'validateInviteToken', + field: 'rid', + }); + } + if (inviteData.expires && new Date(inviteData.expires).getTime() <= Date.now()) { throw new Meteor.Error('error-invite-expired', 'The invite token has expired.', { method: 'validateInviteToken', diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 26d4948bd0f19..b86dff943e45f 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -3,10 +3,10 @@ import { Settings } from '@rocket.chat/models'; import moment from 'moment'; import Queue from 'queue-fifo'; -import { callbacks } from '../../../../lib/callbacks'; -import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; -import { afterLogoutCleanUpCallback } from '../../../../lib/callbacks/afterLogoutCleanUpCallback'; import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { afterLeaveRoomCallback } from '../../../../server/lib/callbacks/afterLeaveRoomCallback'; +import { afterLogoutCleanUpCallback } from '../../../../server/lib/callbacks/afterLogoutCleanUpCallback'; import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as servers from '../servers'; diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index 85117da3db345..0969ea8d9cf36 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -1,5 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { clientCallbacks } from '@rocket.chat/ui-client'; import { Meteor } from 'meteor/meteor'; import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; @@ -7,7 +8,6 @@ import { settings } from '../../../../client/lib/settings'; import { dispatchToastMessage } from '../../../../client/lib/toast'; import { getUser, getUserId } from '../../../../client/lib/user'; import { Messages, Rooms } from '../../../../client/stores'; -import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { t } from '../../../utils/lib/i18n'; @@ -44,7 +44,7 @@ Meteor.methods({ await onClientMessageReceived(message as IMessage).then((message) => { Messages.state.store(message); - return callbacks.run('afterSaveMessage', message, { room, user }); + return clientCallbacks.run('afterSaveMessage', message, { room, user }); }); }, }); diff --git a/apps/meteor/app/lib/lib/MessageTypes.ts b/apps/meteor/app/lib/lib/MessageTypes.ts index 2d83584da5c08..8205200615ff7 100644 --- a/apps/meteor/app/lib/lib/MessageTypes.ts +++ b/apps/meteor/app/lib/lib/MessageTypes.ts @@ -29,6 +29,14 @@ export const MessageTypesValues: Array<{ key: MessageTypesValuesType; i18nLabel: key: 'au', // added user i18nLabel: 'Message_HideType_au', }, + { + key: 'ui', // user invited to room + i18nLabel: 'Message_HideType_ui', + }, + { + key: 'uir', // user rejected invitation to room + i18nLabel: 'Message_HideType_uir', + }, { key: 'added-user-to-team', i18nLabel: 'Message_HideType_added_user_to_team', diff --git a/apps/meteor/app/lib/server/functions/acceptRoomInvite.ts b/apps/meteor/app/lib/server/functions/acceptRoomInvite.ts new file mode 100644 index 0000000000000..6a406055dfd5d --- /dev/null +++ b/apps/meteor/app/lib/server/functions/acceptRoomInvite.ts @@ -0,0 +1,61 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; +import { Message } from '@rocket.chat/core-services'; +import type { IUser, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { Subscriptions, Users } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { callbacks } from '../../../../server/lib/callbacks'; +import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; + +/** + * Accepts a room invite when triggered by internal events such as federation + * or third-party callbacks. Performs the necessary database updates and triggers + * safe callbacks, ensuring no propagation loops are created during external event + * processing. + */ + +// TODO this funcion is pretty much the same as the one in addUserToRoom.ts, we should probably +// unify them at some point +export const performAcceptRoomInvite = async ( + room: IRoom, + subscription: ISubscription, + user: IUser & { username: string }, +): Promise => { + if (subscription.status !== 'INVITED' || !subscription.inviter) { + throw new Meteor.Error('error-not-invited', `User was not invited to this room ${subscription.status}`); + } + const inviter = await Users.findOneById(subscription.inviter._id); + + await callbacks.run('beforeJoinRoom', user, room); + + await callbacks.run('beforeAddedToRoom', { user, inviter }, room); + + try { + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter); + } catch (error: any) { + if (error.name === AppsEngineException.name) { + throw new Meteor.Error('error-app-prevented', error.message); + } + + throw error; + } + + await Subscriptions.acceptInvitationById(subscription._id); + + void notifyOnSubscriptionChangedById(subscription._id, 'updated'); + + await Message.saveSystemMessage('uj', room._id, user.username, user); + + if (room.t === 'c' || room.t === 'p') { + process.nextTick(async () => { + // Add a new event, with an optional inviter + await callbacks.run('afterAddedToRoom', { user, inviter }, room); + + // Keep the current event + await callbacks.run('afterJoinRoom', user, room); + + void Apps.self?.triggerEvent(AppEvents.IPostRoomUserJoined, room, user, inviter); + }); + } +}; diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index f5317a7b0e3b3..89f4e5e352755 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -3,8 +3,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; import { getDefaultChannels } from './getDefaultChannels'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; @@ -13,6 +14,10 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: const defaultRooms = await getDefaultChannels(); for await (const room of defaultRooms) { + if (settings.get('ABAC_Enabled') && room?.abacAttributes?.length) { + continue; + } + if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 881afb11812c7..6ebfd84844878 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -1,18 +1,17 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team } from '@rocket.chat/core-services'; -import { type IUser } from '@rocket.chat/core-typings'; +import { Team, Room } from '@rocket.chat/core-services'; +import { isRoomNativeFederated, type IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { callbacks } from '../../../../lib/callbacks'; -import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; -import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { beforeAddUserToRoom } from '../../../../server/lib/callbacks/beforeAddUserToRoom'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { settings } from '../../../settings/server'; -import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; -import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; +import { beforeAddUserToRoom as beforeAddUserToRoomPatch } from '../lib/beforeAddUserToRoom'; +import { notifyOnRoomChangedById } from '../lib/notifyListener'; /** * This function adds user to the given room. @@ -49,6 +48,12 @@ export const addUserToRoom = async ( throw new Meteor.Error('user-not-found'); } + // Check if user is already in room + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); + if (subscription) { + return; + } + if ( !(await roomDirectives.allowMemberAction(room, RoomMemberActions.JOIN, userToBeAdded._id)) && !(await roomDirectives.allowMemberAction(room, RoomMemberActions.INVITE, userToBeAdded._id)) @@ -57,7 +62,10 @@ export const addUserToRoom = async ( } try { - await beforeAddUserToRoom.run({ user: userToBeAdded, inviter: (inviter && (await Users.findOneById(inviter._id))) || undefined }, room); + const inviterUser = inviter && ((await Users.findOneById(inviter._id)) || undefined); + // Not "duplicated": we're moving away from callbacks so this is a patch function. We should migrate the next one to be a patch or use this same patch, instead of calling both + await beforeAddUserToRoomPatch([userToBeAdded.username!], room, inviterUser); + await beforeAddUserToRoom.run({ user: userToBeAdded, inviter: inviterUser }, room); } catch (error) { throw new Meteor.Error((error as any)?.message); } @@ -66,12 +74,6 @@ export const addUserToRoom = async ( await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter }); - // Check if user is already in room - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); - if (subscription || !userToBeAdded) { - return; - } - try { await Apps.self?.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); } catch (error: any) { @@ -81,6 +83,12 @@ export const addUserToRoom = async ( throw error; } + + // for federation rooms we stop here since everything else will be handled by the federation invite flow + if (isRoomNativeFederated(room)) { + return; + } + // TODO: are we calling this twice? if (room.t === 'c' || room.t === 'p' || room.t === 'l') { // Add a new event, with an optional inviter @@ -90,50 +98,16 @@ export const addUserToRoom = async ( await callbacks.run('beforeJoinRoom', userToBeAdded, room); } - const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded); - - const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { + await Room.createUserSubscription({ + room, ts: now, - open: !createAsHidden, - alert: createAsHidden ? false : !skipAlertSound, - unread: 1, - userMentions: 1, - groupMentions: 0, - ...autoTranslateConfig, - ...getDefaultSubscriptionPref(userToBeAdded as IUser), + inviter, + userToBeAdded, + createAsHidden, + skipAlertSound, + skipSystemMessage, }); - if (insertedId) { - void notifyOnSubscriptionChangedById(insertedId, 'inserted'); - } - - if (!userToBeAdded.username) { - throw new Meteor.Error('error-invalid-user', 'Cannot add an user to a room without a username'); - } - - if (!skipSystemMessage) { - if (inviter) { - const extraData = { - ts: now, - u: { - _id: inviter._id, - username: inviter.username, - }, - }; - if (room.teamMain) { - await Message.saveSystemMessage('added-user-to-team', rid, userToBeAdded.username, userToBeAdded, extraData); - } else { - await Message.saveSystemMessage('au', rid, userToBeAdded.username, userToBeAdded, extraData); - } - } else if (room.prid) { - await Message.saveSystemMessage('ut', rid, userToBeAdded.username, userToBeAdded, { ts: now }); - } else if (room.teamMain) { - await Message.saveSystemMessage('ujt', rid, userToBeAdded.username, userToBeAdded, { ts: now }); - } else { - await Message.saveSystemMessage('uj', rid, userToBeAdded.username, userToBeAdded, { ts: now }); - } - } - if (room.t === 'c' || room.t === 'p') { process.nextTick(async () => { // Add a new event, with an optional inviter diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts index 46fd7a1ac35ba..9bc3582d6671d 100644 --- a/apps/meteor/app/lib/server/functions/archiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/archiveRoom.ts @@ -2,7 +2,7 @@ import { Message } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnRoomChanged, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise { diff --git a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts index e66c0ab3edd2a..49aa6e44d55b5 100644 --- a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts +++ b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts @@ -1,7 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 7f26a78d85089..029cebc352d50 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -7,8 +7,9 @@ import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; import type { MatchKeysAndValues } from 'mongodb'; -import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { getNameForDMs } from '../../../../server/services/room/getNameForDMs'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../lib/notifyListener'; @@ -37,16 +38,13 @@ const generateSubscription = ( }, }); -const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); -const getName = (members: IUser[]): string => members.map(({ username }) => username).join(', '); - export async function createDirectRoom( members: IUser[] | string[], roomExtraData: Partial = {}, options: { - creator?: string; + forceNew?: boolean; + creator?: IUser['_id']; subscriptionExtra?: ISubscriptionExtraData; - federatedRoomId?: string; }, ): Promise { const maxUsers = settings.get('DirectMesssage_maxUsers') || 1; @@ -72,6 +70,7 @@ export async function createDirectRoom( await callbacks.run('beforeCreateDirectRoom', membersUsernames, roomExtraData); const roomMembers = await Users.findUsersByUsernames(membersUsernames).toArray(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const sortedMembers = roomMembers.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); @@ -79,15 +78,11 @@ export async function createDirectRoom( const uids = roomMembers.map(({ _id }) => _id).sort(); // Deprecated: using users' _id to compose the room _id is deprecated - const room: IRoom | null = - uids.length === 2 - ? await Rooms.findOneById(uids.join(''), { projection: { _id: 1 } }) - : await Rooms.findOneDirectRoomContainingAllUserIDs(uids, { projection: { _id: 1 } }); + const room: IRoom | null = options?.forceNew ? null : await Rooms.findOneDirectRoomContainingAllUserIDs(uids, { projection: { _id: 1 } }); const isNewRoom = !room; const roomInfo = { - ...(uids.length === 2 && { _id: uids.join('') }), // Deprecated: using users' _id to compose the room _id is deprecated t: 'd', usernames, usersCount: members.length, @@ -155,15 +150,40 @@ export async function createDirectRoom( { projection: { 'username': 1, 'settings.preferences': 1 } }, ).toArray(); + const creatorUser = roomMembers.find((member) => member._id === options?.creator); + if (roomExtraData.federated && !creatorUser) { + throw new Meteor.Error('error-creator-not-in-room', 'The creator user must be part of the direct room'); + } + + const roomNames = getNameForDMs(roomMembers); + for await (const member of membersWithPreferences) { - const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); + const subscriptionStatus: Partial = + roomExtraData.federated && options.creator !== member._id && creatorUser + ? { + status: 'INVITED', + inviter: { + _id: creatorUser._id, + username: creatorUser.username!, + name: creatorUser.name, + }, + open: true, + unread: 1, + userMentions: 1, + } + : {}; + + const { fname, name } = roomNames[member._id]; + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': member._id }, { ...(options?.creator === member._id && { $set: { open: true } }), - $setOnInsert: generateSubscription(getFname(otherMembers), getName(otherMembers), member, { + $setOnInsert: generateSubscription(fname, name, member, { ...options?.subscriptionExtra, ...(options?.creator !== member._id && { open: members.length > 2 }), + ...subscriptionStatus, + ...(roomExtraData.federated && member._id === options?.creator && { roles: ['owner'] }), }), }, { upsert: true }, @@ -181,7 +201,6 @@ export async function createDirectRoom( await callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator, - mrid: options?.federatedRoomId, }); void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, insertedRoom); diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 3d9e3b1810570..7b64ec9e68b09 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,17 +1,16 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team } from '@rocket.chat/core-services'; +import { FederationMatrix, Message, Room, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; -import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { type ICreatedRoom, type IUser, type IRoom, type RoomType, isUserNativeFederated } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { createDirectRoom } from './createDirectRoom'; -import { callbacks } from '../../../../lib/callbacks'; -import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; -import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; import { calculateRoomRolePriorityFromRoles } from '../../../../lib/roles/calculateRoomRolePriorityFromRoles'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { beforeAddUserToRoom } from '../../../../server/lib/callbacks/beforeAddUserToRoom'; +import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../../../../server/lib/callbacks/beforeCreateRoomCallback'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -59,6 +58,27 @@ async function createUsersSubscriptions({ await notifyOnRoomChanged(room, 'inserted'); } + // Invite federated members to the room SYNCRONOUSLY, + // since we do not use to invite lots of users at once, this is acceptable. + const membersToInvite = members.filter((m) => m !== owner.username); + + await FederationMatrix.ensureFederatedUsersExistLocally(membersToInvite); + + for await (const memberUsername of membersToInvite) { + const member = await Users.findOneByUsername(memberUsername); + if (!member) { + throw new Error('Federated user not found locally'); + } + + await Room.createUserSubscription({ + ts: new Date(), + room, + userToBeAdded: member, + inviter: owner, + status: 'INVITED', + }); + } + return; } @@ -135,7 +155,7 @@ export const createRoom = async ( rid: string; } > => { - const { teamId, ...optionalExtraData } = roomExtraData || ({} as IRoom); + const { teamId, ...extraData } = roomExtraData || ({} as IRoom); // TODO: use a shared helper to check whether a user is federated const hasFederatedMembers = members.some((member) => { @@ -146,23 +166,12 @@ export const createRoom = async ( }); // Prevent adding federated users to rooms that are not marked as federated explicitly - if (hasFederatedMembers && optionalExtraData.federated !== true) { + if (hasFederatedMembers && extraData.federated !== true) { throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', { method: 'createRoom', }); } - const extraData = { - ...optionalExtraData, - ...((hasFederatedMembers || optionalExtraData.federated) && { - federated: true, - federation: { - version: 1, - // TODO we should be able to provide all values from here, currently we update on callback afterCreateRoom - }, - }), - }; - await prepareCreateRoomCallback.run({ type, // name, @@ -173,15 +182,21 @@ export const createRoom = async ( // options, }); - const shouldBeHandledByFederation = isRoomNativeFederated(extraData); - if (shouldBeHandledByFederation && owner && !(await hasPermissionAsync(owner._id, 'access-federation'))) { + const shouldBeHandledByFederation = extraData.federated === true; + + if ( + shouldBeHandledByFederation && + owner && + !isUserNativeFederated(owner) && + !(await hasPermissionAsync(owner._id, 'access-federation')) + ) { throw new Meteor.Error('error-not-authorized-federation', 'Not authorized to access federation', { method: 'createRoom', }); } if (type === 'd') { - return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username }); + return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?._id }); } if (!onlyUsernames(members)) { @@ -286,6 +301,13 @@ export const createRoom = async ( void notifyOnRoomChanged(room, 'inserted'); + // If federated, we must create Matrix room BEFORE subscriptions so invites can be sent. + if (shouldBeHandledByFederation) { + // Reusing unused callback to create Matrix room. + // We should discuss the opportunity to rename it to something with "before" prefix. + await callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members, options }); + } + await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); if (type === 'c') { @@ -301,10 +323,6 @@ export const createRoom = async ( } callbacks.runAsync('afterCreateRoom', owner, room); - if (shouldBeHandledByFederation) { - callbacks.runAsync('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members, options }); - } - void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, room); return { rid: room._id, // backwards compatible diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 0be9a8b78e9ba..1c3fb32ef28bd 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -4,7 +4,7 @@ import { isThreadMessage, type AtLeast, type IMessage, type IRoom, type IThreadM import { Messages, Rooms, Uploads, Users, ReadReceipts, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/lib/server/functions/deleteRoom.ts b/apps/meteor/app/lib/server/functions/deleteRoom.ts index 386ba8da8b94d..d9af32b4faa82 100644 --- a/apps/meteor/app/lib/server/functions/deleteRoom.ts +++ b/apps/meteor/app/lib/server/functions/deleteRoom.ts @@ -1,6 +1,6 @@ import { Messages, Rooms, Subscriptions } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { FileUpload } from '../../../file-upload/server'; import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 925dffb84ef3d..02847707a8765 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -20,7 +20,7 @@ import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { updateGroupDMsName } from './updateGroupDMsName'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index c55a5464a2a8b..f1e1be0af0499 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -19,9 +19,10 @@ export const defaultFields = { reason: 1, statusText: 1, avatarETag: 1, - extension: 1, federated: 1, statusLivechat: 1, + abacAttributes: 1, + freeSwitchExtension: 1, } as const; export const fullFields = { @@ -35,7 +36,6 @@ export const fullFields = { requirePasswordChangeReason: 1, roles: 1, importIds: 1, - freeSwitchExtension: 1, } as const; let publicCustomFields: Record = {}; @@ -86,7 +86,6 @@ export async function getFullUserDataByIdOrUsernameOrImportId( (searchType === 'username' && searchValue === caller.username) || (searchType === 'importId' && caller.importIds?.includes(searchValue)); const canViewAllInfo = !!myself || (await hasPermissionAsync(userId, 'view-full-other-user-info')); - const canViewExtension = !!myself || (await hasPermissionAsync(userId, 'view-user-voip-extension')); // Only search for importId if the user has permission to view the import id if (searchType === 'importId' && !canViewAllInfo) { @@ -98,7 +97,6 @@ export async function getFullUserDataByIdOrUsernameOrImportId( const options = { projection: { ...fields, - ...(canViewExtension && { freeSwitchExtension: 1 }), ...(myself && { services: 1 }), }, }; diff --git a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts index 63e6dc52b9142..2a14900e10a65 100644 --- a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts +++ b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts @@ -1,12 +1,14 @@ -export const getModifiedHttpHeaders = (httpHeaders: Headers | Record) => { - let modifiedHttpHeaders: { [k: string]: string }; - +export const normalizeHeaders = (httpHeaders?: Headers | Record) => { if (httpHeaders instanceof Headers) { - modifiedHttpHeaders = { ...Object.fromEntries(httpHeaders.entries()) }; - } else { - modifiedHttpHeaders = { ...httpHeaders }; + return { ...Object.fromEntries(httpHeaders.entries()) }; } + return { ...httpHeaders }; +}; + +export const getModifiedHttpHeaders = (httpHeaders: Headers | Record) => { + const modifiedHttpHeaders = normalizeHeaders(httpHeaders); + if ('x-auth-token' in modifiedHttpHeaders) { modifiedHttpHeaders['x-auth-token'] = '[redacted]'; } diff --git a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts index e1aeabe1b46a5..6217342d0c534 100644 --- a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts +++ b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts @@ -14,7 +14,7 @@ export const getRoomByNameOrIdWithOptionToJoin = async ({ joinChannel = true, errorOnEmpty = true, }: { - user: Pick; + user: Pick; nameOrId: string; type?: RoomType; tryDirectByUserIdOnly?: boolean; diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 8fa1eb02a29fc..fa99f7eeb2048 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -1,8 +1,8 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../../lib/callbacks'; import { ltrim } from '../../../../../lib/utils/stringUtils'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { roomCoordinator } from '../../../../../server/lib/rooms/roomCoordinator'; import * as Mailer from '../../../../mailer/server/api'; diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts index 538ff2ad5ed91..47a38ac0f068c 100644 --- a/apps/meteor/app/lib/server/functions/notifications/index.ts +++ b/apps/meteor/app/lib/server/functions/notifications/index.ts @@ -2,7 +2,7 @@ import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { settings } from '../../../../settings/server'; diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 23e82389cb271..b58710aaeb5e1 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -1,65 +1,67 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team, Room } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; -import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback'; +import { afterLeaveRoomCallback } from '../../../../server/lib/callbacks/afterLeaveRoomCallback'; +import { beforeLeaveRoomCallback } from '../../../../server/lib/callbacks/beforeLeaveRoomCallback'; import { settings } from '../../../settings/server'; import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; -export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise { - const room = await Rooms.findOneById(rid); - - if (!room) { +/** + * Removes a user from a room when triggered by federation or other external events. + * Executes only the necessary database operations, with no callbacks, to prevent + * propagation loops during external event processing. + */ +export const performUserRemoval = async function ( + room: IRoom, + user: IUser, + options?: { byUser?: IUser; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues }, +): Promise { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { + projection: { _id: 1, status: 1 }, + }); + if (!subscription) { return; } - try { - await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser); - } catch (error: any) { - if (error.name === AppsEngineException.name) { - throw new Meteor.Error('error-app-prevented', error.message); - } - - throw error; + // make TS happy, this should never happen + if (!user.username) { + throw new Error('User must have a username to be removed from the room'); } - await Room.beforeLeave(room); - // TODO: move before callbacks to service await beforeLeaveRoomCallback.run(user, room); - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { - projection: { _id: 1 }, - }); - if (subscription) { - const removedUser = user; - if (options?.byUser) { + if (options?.customSystemMessage) { + await Message.saveSystemMessage(options?.customSystemMessage, room._id, user.username || '', user); + } else if (options?.byUser) { const extraData = { u: options.byUser, }; if (room.teamMain) { - await Message.saveSystemMessage('removed-user-from-team', rid, user.username || '', user, extraData); + await Message.saveSystemMessage('removed-user-from-team', room._id, user.username, user, extraData); } else { - await Message.saveSystemMessage('ru', rid, user.username || '', user, extraData); + await Message.saveSystemMessage('ru', room._id, user.username, user, extraData); } + } else if (subscription.status === 'INVITED') { + await Message.saveSystemMessage('uir', room._id, user.username, user); } else if (room.teamMain) { - await Message.saveSystemMessage('ult', rid, removedUser.username || '', removedUser); + await Message.saveSystemMessage('ult', room._id, user.username, user); } else { - await Message.saveSystemMessage('ul', rid, removedUser.username || '', removedUser); + await Message.saveSystemMessage('ul', room._id, user.username, user); } } if (room.t === 'l') { - await Message.saveSystemMessage('command', rid, 'survey', user); + await Message.saveSystemMessage('command', room._id, 'survey', user); } - const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(room._id, user._id); if (deletedSubscription) { void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); } @@ -72,10 +74,47 @@ export const removeUserFromRoom = async function (rid: string, user: IUser, opti await Rooms.removeUsersFromE2EEQueueByRoomId(room._id, [user._id]); } - // TODO: CACHE: maybe a queue? - await afterLeaveRoomCallback.run({ user, kicker: options?.byUser }, room); + // remove references to the user in direct message rooms + if (room.t === 'd') { + await Rooms.removeUserReferenceFromDMsById(room._id, user.username, user._id); + } + + void notifyOnRoomChangedById(room._id); +}; - void notifyOnRoomChangedById(rid); +/** + * Removes a user from the given room by performing the required database updates + * and triggering all standard callbacks. Used for local actions (UI or API) + * that should propagate normally to federation and other subscribers. + */ +export const removeUserFromRoom = async function ( + rid: string, + user: IUser, + options?: { byUser?: IUser; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues }, +): Promise { + const room = await Rooms.findOneById(rid); + if (!room) { + return; + } + + // Rationale: for an abac room, we don't want apps to be able to prevent a user from leaving + if (!options?.skipAppPreEvents) { + try { + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser); + } catch (error: any) { + if (error.name === AppsEngineException.name) { + throw new Meteor.Error('error-app-prevented', error.message); + } + + throw error; + } + } + + await Room.beforeLeave(room); + + await performUserRemoval(room, user, options); + + await afterLeaveRoomCallback.run({ user, kicker: options?.byUser }, room); await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user, options?.byUser); }; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index e77e56cc84b9d..6665177794e73 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -6,9 +6,17 @@ import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; -import { callbacks } from '../../../../../lib/callbacks'; +import { handleBio } from './handleBio'; +import { handleNickname } from './handleNickname'; +import { saveNewUser } from './saveNewUser'; +import { sendPasswordEmail } from './sendUserEmail'; +import { setPasswordUpdater } from './setPasswordUpdater'; +import { validateUserData } from './validateUserData'; +import { validateUserEditing } from './validateUserEditing'; import { wrapInSessionTransaction, onceTransactionCommitedSuccessfully } from '../../../../../server/database/utils'; import type { UserChangedAuditStore } from '../../../../../server/lib/auditServerEvents/userChanged'; +import { callbacks } from '../../../../../server/lib/callbacks'; +import { shouldBreakInVersion } from '../../../../../server/lib/shouldBreakInVersion'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser'; import { generatePassword } from '../../lib/generatePassword'; @@ -18,14 +26,6 @@ import { saveCustomFields } from '../saveCustomFields'; import { saveUserIdentity } from '../saveUserIdentity'; import { setEmail } from '../setEmail'; import { setStatusText } from '../setStatusText'; -import { handleBio } from './handleBio'; -import { handleNickname } from './handleNickname'; -import { saveNewUser } from './saveNewUser'; -import { sendPasswordEmail } from './sendUserEmail'; -import { setPasswordUpdater } from './setPasswordUpdater'; -import { validateUserData } from './validateUserData'; -import { validateUserEditing } from './validateUserEditing'; -import { shouldBreakInVersion } from '../../../../../server/lib/shouldBreakInVersion'; export type SaveUserData = { _id?: IUser['_id']; @@ -125,7 +125,7 @@ const _saveUser = (session?: ClientSession) => } if (typeof userData.statusText === 'string') { - await setStatusText(userData._id, userData.statusText, updater, session); + await setStatusText(userData._id, userData.statusText, { updater, session }); } if (userData.email) { diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts index 2fd4a3f0e6f42..529568854c68c 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts @@ -12,12 +12,8 @@ const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['rol (newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item))); const isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue; -export const canEditExtension = async (userId: string, newExtension?: string) => { - if (!settings.get('VoIP_TeamCollab_Enabled')) { - return false; - } - - if (!(await hasPermissionAsync(userId, 'manage-voip-extensions'))) { +export const canEditExtension = async (newExtension?: string) => { + if (!settings.get('VoIP_TeamCollab_SIP_Integration_Enabled')) { return false; } @@ -117,7 +113,7 @@ export async function validateUserEditing(userId: IUser['_id'], userData: Update if ( isEditingField(user.freeSwitchExtension ?? '', userData.freeSwitchExtension) && - !(await canEditExtension(userId, userData.freeSwitchExtension)) + !(await canEditExtension(userData.freeSwitchExtension)) ) { throw new MeteorError('error-action-not-allowed', 'Edit user voice call extension is not allowed', { method: 'insertOrUpdateUser', diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 62c127b46325d..e943e1b9128ef 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; -import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users, CallHistory } from '@rocket.chat/models'; import type { ClientSession } from 'mongodb'; import { _setRealName } from './setRealName'; @@ -181,5 +181,8 @@ async function updateUsernameReferences({ // update name and username of users on video conferences await VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName); + + // update name and username of users on call history + await CallHistory.updateUserReferences(user._id, username || previousUsername, name || previousName); } } diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index adcd9592b3ae3..6af459f2332f1 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,5 +1,5 @@ import { Apps } from '@rocket.chat/apps'; -import { api, Message } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -79,8 +79,8 @@ const validateAttachmentsFields = (attachmentField: any) => { }), ); - if (typeof attachmentField.value !== 'undefined') { - attachmentField.value = String(attachmentField.value); + if (!attachmentField.value || !attachmentField.title) { + throw new Error('Invalid attachment field, title and value is required'); } }; @@ -225,11 +225,6 @@ export const sendMessage = async function (user: any, message: any, room: any, u await validateMessage(message, room, user); prepareMessageObject(message, room._id, user); - if (message.t === 'otr') { - void api.broadcast('otrMessage', { roomId: message.rid, message, user, room }); - return message; - } - if (settings.get('Message_Read_Receipt_Enabled')) { message.unread = true; } diff --git a/apps/meteor/app/lib/server/functions/setStatusText.ts b/apps/meteor/app/lib/server/functions/setStatusText.ts index 7c81bae0112ed..8a5276d584ead 100644 --- a/apps/meteor/app/lib/server/functions/setStatusText.ts +++ b/apps/meteor/app/lib/server/functions/setStatusText.ts @@ -9,7 +9,19 @@ import { onceTransactionCommitedSuccessfully } from '../../../../server/database import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { RateLimiter } from '../lib'; -async function _setStatusText(userId: string, statusText: string, updater?: Updater, session?: ClientSession): Promise { +async function _setStatusText( + userId: string, + statusText: string, + { + updater, + session, + emit = true, + }: { + updater?: Updater; + session?: ClientSession; + emit?: boolean; + } = {}, +): Promise { if (!userId) { return false; } @@ -35,13 +47,15 @@ async function _setStatusText(userId: string, statusText: string, updater?: Upda await Users.updateStatusText(user._id, statusText, { session }); } - const { _id, username, status, name, roles } = user; - await onceTransactionCommitedSuccessfully(() => { - void api.broadcast('presence.status', { - user: { _id, username, status, statusText, name, roles }, - previousStatus: status, - }); - }, session); + if (emit) { + const { _id, username, status, name, roles } = user; + await onceTransactionCommitedSuccessfully(() => { + void api.broadcast('presence.status', { + user: { _id, username, status, statusText, name, roles }, + previousStatus: status, + }); + }, session); + } return true; } diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index fa59df01ba589..8ebfe0c7449bc 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -9,7 +9,7 @@ import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 761549ed10287..ad0364d5617a0 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -15,8 +15,8 @@ import { joinDefaultChannels } from './joinDefaultChannels'; import { saveUserIdentity } from './saveUserIdentity'; import { setUserAvatar } from './setUserAvatar'; import { validateUsername } from './validateUsername'; -import { callbacks } from '../../../../lib/callbacks'; import { onceTransactionCommitedSuccessfully } from '../../../../server/database/utils'; +import { callbacks } from '../../../../server/lib/callbacks'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { notifyOnUserChange } from '../lib/notifyListener'; diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts index f56827baeb8ac..9fb7c341b83e4 100644 --- a/apps/meteor/app/lib/server/index.ts +++ b/apps/meteor/app/lib/server/index.ts @@ -16,7 +16,6 @@ import './methods/checkRegistrationSecretURL'; import './methods/cleanRoomHistory'; import './methods/createChannel'; import './methods/createPrivateGroup'; -import './methods/createToken'; import './methods/deleteUserOwnAccount'; import './methods/executeSlashCommandPreview'; import './startup/mentionUserNotInChannel'; @@ -27,8 +26,6 @@ import './methods/getSingleMessage'; import './methods/getMessages'; import './methods/getSlashCommandPreviews'; import './methods/getUsernameSuggestion'; -import './methods/getUserRoles'; -import './methods/insertOrUpdateUser'; import './methods/joinDefaultChannels'; import './methods/joinRoom'; import './methods/leaveRoom'; @@ -39,14 +36,11 @@ import './methods/saveSetting'; import './methods/saveSettings'; import './methods/sendMessage'; import './methods/sendSMTPTestEmail'; -import './methods/setAdminStatus'; import './methods/setEmail'; import './methods/setRealName'; -import './methods/setUsername'; import './methods/unarchiveRoom'; import './methods/unblockUser'; import './methods/updateMessage'; -import './methods/saveCustomFields'; import './methods/checkFederationConfiguration'; export * from './lib'; diff --git a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts index 910b59d76e8a4..b320c2d87e8d6 100644 --- a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts +++ b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts @@ -2,7 +2,7 @@ import type { IMessage, IUser, IRoom } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Rooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; export async function afterSaveMessage(message: IMessage, room: IRoom, user: IUser, roomUpdater?: Updater): Promise { const updater = roomUpdater ?? Rooms.getUpdater(); diff --git a/apps/meteor/app/lib/server/lib/beforeAddUserToRoom.ts b/apps/meteor/app/lib/server/lib/beforeAddUserToRoom.ts new file mode 100644 index 0000000000000..984dc34a22a12 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/beforeAddUserToRoom.ts @@ -0,0 +1,6 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export const beforeAddUserToRoom = makeFunction(async (_users: IUser['username'][], _room: IRoom, _actor?: IUser) => { + // no op on CE +}); diff --git a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts index ee95c9d2fa328..6a7fc267caaa4 100644 --- a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts +++ b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts @@ -24,13 +24,17 @@ const compareVersions = (version: string, message: string) => { } }; -export type DeprecationLoggerNextPlannedVersion = '7.0.0' | '8.0.0'; +export type DeprecationLoggerNextPlannedVersion = '9.0.0'; export const apiDeprecationLogger = ((logger) => { return { endpoint: (endpoint: string, version: DeprecationLoggerNextPlannedVersion, res: Response, info = '') => { const message = `The endpoint "${endpoint}" is deprecated and will be removed on version ${version}${info ? ` (${info})` : ''}`; + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } + compareVersions(version, message); writeDeprecationHeader(res, 'endpoint-deprecation', message, version); @@ -54,6 +58,10 @@ export const apiDeprecationLogger = ((logger) => { }) ?? `The parameter "${parameter}" in the endpoint "${endpoint}" is deprecated and will be removed on version ${version}`; compareVersions(version, message); + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } + metrics.deprecations.inc({ type: 'parameter-deprecation', kind: 'endpoint', name: endpoint, params: parameter }); writeDeprecationHeader(res, 'parameter-deprecation', message, version); @@ -76,6 +84,11 @@ export const apiDeprecationLogger = ((logger) => { endpoint, version, }) ?? `The usage of the endpoint "${endpoint}" is deprecated and will be removed on version ${version}`; + + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } + compareVersions(version, message); metrics.deprecations.inc({ type: 'invalid-usage', kind: 'endpoint', name: endpoint, params: parameter }); @@ -96,12 +109,18 @@ export const methodDeprecationLogger = ((logger) => { ) => { const replacement = typeof info === 'string' ? info : `Use the ${info} endpoint instead`; const message = `The method "${method}" is deprecated and will be removed on version ${version}${replacement ? ` (${replacement})` : ''}`; + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } compareVersions(version, message); metrics.deprecations.inc({ type: 'deprecation', name: method, kind: 'method' }); logger.warn(message); }, parameter: (method: string, parameter: string, version: DeprecationLoggerNextPlannedVersion) => { const message = `The parameter "${parameter}" in the method "${method}" is deprecated and will be removed on version ${version}`; + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } metrics.deprecations.inc({ type: 'parameter-deprecation', name: method, params: parameter }); @@ -122,6 +141,11 @@ export const methodDeprecationLogger = ((logger) => { method, version, }) ?? `The usage of the method "${method}" is deprecated and will be removed on version ${version}`; + + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } + compareVersions(version, message); metrics.deprecations.inc({ type: 'invalid-usage', name: method, params: parameter, kind: 'method' }); @@ -130,6 +154,10 @@ export const methodDeprecationLogger = ((logger) => { }, /** @deprecated */ warn: (message: string) => { + if (process.env.TEST_MODE === 'true') { + throw new Error(message); + } + compareVersions('0.0.0', message); logger.warn(message); }, diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 2b5c10caa662e..210510b001dc6 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -7,7 +7,6 @@ import type { IRole, IPermission, IIntegration, - IPbxEvent, LoginServiceConfiguration as LoginServiceConfigurationData, ILivechatInquiryRecord, ILivechatPriority, @@ -24,12 +23,10 @@ import type { ILivechatContact, } from '@rocket.chat/core-typings'; import { - dbWatchersDisabled, Rooms, LivechatRooms, Permissions, Settings, - PbxEvents, Roles, Integrations, LoginServiceConfiguration, @@ -47,360 +44,327 @@ import { shouldHideSystemMessage } from '../../../../server/lib/systemMessage/hi type ClientAction = 'inserted' | 'updated' | 'removed'; -function withDbWatcherCheck Promise>(fn: T): T { - return dbWatchersDisabled ? fn : ((() => Promise.resolve()) as T); -} - -export const notifyOnLivechatPriorityChanged = withDbWatcherCheck( - async (data: Pick, clientAction: ClientAction = 'updated'): Promise => { - const { _id, ...rest } = data; - void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } }); - }, -); - -export const notifyOnRoomChanged = withDbWatcherCheck( - async (data: T | T[], clientAction: ClientAction = 'updated'): Promise => { - const items = Array.isArray(data) ? data : [data]; - for (const item of items) { - void api.broadcast('watch.rooms', { clientAction, room: item }); - } - }, -); +export const notifyOnLivechatPriorityChanged = async ( + data: Pick, + clientAction: ClientAction = 'updated', +): Promise => { + const { _id, ...rest } = data; + void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } }); +}; -export const notifyOnRoomChangedById = withDbWatcherCheck( - async (ids: T['_id'] | T['_id'][], clientAction: ClientAction = 'updated'): Promise => { - const eligibleIds = Array.isArray(ids) ? ids : [ids]; - const items = Rooms.findByIds(eligibleIds); - for await (const item of items) { - void api.broadcast('watch.rooms', { clientAction, room: item }); - } - }, -); +export const notifyOnRoomChanged = async ( + data: T | T[], + clientAction: ClientAction = 'updated', +): Promise => { + const items = Array.isArray(data) ? data : [data]; + for (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +}; -export const notifyOnRoomChangedByUsernamesOrUids = withDbWatcherCheck( - async ( - uids: T['u']['_id'][], - usernames: T['u']['username'][], - clientAction: ClientAction = 'updated', - ): Promise => { - const items = Rooms.findByUsernamesOrUids(uids, usernames); - for await (const item of items) { - void api.broadcast('watch.rooms', { clientAction, room: item }); - } - }, -); +export const notifyOnRoomChangedById = async ( + ids: T['_id'] | T['_id'][], + clientAction: ClientAction = 'updated', +): Promise => { + const eligibleIds = Array.isArray(ids) ? ids : [ids]; + const items = Rooms.findByIds(eligibleIds); + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +}; -export const notifyOnRoomChangedByContactId = withDbWatcherCheck( - async (contactId: T['_id'], clientAction: ClientAction = 'updated'): Promise => { - const cursor = LivechatRooms.findOpenByContactId(contactId); +export const notifyOnRoomChangedByUsernamesOrUids = async ( + uids: T['u']['_id'][], + usernames: T['u']['username'][], + clientAction: ClientAction = 'updated', +): Promise => { + const items = Rooms.findByUsernamesOrUids(uids, usernames); + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +}; - void cursor.forEach((room) => { - void api.broadcast('watch.rooms', { clientAction, room }); - }); - }, -); +export const notifyOnRoomChangedByContactId = async ( + contactId: T['_id'], + clientAction: ClientAction = 'updated', +): Promise => { + const cursor = LivechatRooms.findOpenByContactId(contactId); -export const notifyOnRoomChangedByUserDM = withDbWatcherCheck( - async (userId: T['u']['_id'], clientAction: ClientAction = 'updated'): Promise => { - const items = Rooms.findDMsByUids([userId]); - for await (const item of items) { - void api.broadcast('watch.rooms', { clientAction, room: item }); - } - }, -); + void cursor.forEach((room) => { + void api.broadcast('watch.rooms', { clientAction, room }); + }); +}; -export const notifyOnPermissionChanged = withDbWatcherCheck( - async (permission: IPermission, clientAction: ClientAction = 'updated'): Promise => { - void api.broadcast('permission.changed', { clientAction, data: permission }); +export const notifyOnRoomChangedByUserDM = async ( + userId: T['u']['_id'], + clientAction: ClientAction = 'updated', +): Promise => { + const items = Rooms.findDMsByUids([userId]); + for await (const item of items) { + void api.broadcast('watch.rooms', { clientAction, room: item }); + } +}; - if (permission.level === 'settings' && permission.settingId) { - const setting = await Settings.findOneNotHiddenById(permission.settingId); - if (!setting) { - return; - } - void notifyOnSettingChanged(setting, 'updated'); - } - }, -); +export const notifyOnPermissionChanged = async (permission: IPermission, clientAction: ClientAction = 'updated'): Promise => { + void api.broadcast('permission.changed', { clientAction, data: permission }); -export const notifyOnPermissionChangedById = withDbWatcherCheck( - async (pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise => { - const permission = await Permissions.findOneById(pid); - if (!permission) { + if (permission.level === 'settings' && permission.settingId) { + const setting = await Settings.findOneNotHiddenById(permission.settingId); + if (!setting) { return; } + void notifyOnSettingChanged(setting, 'updated'); + } +}; - return notifyOnPermissionChanged(permission, clientAction); - }, -); +export const notifyOnPermissionChangedById = async (pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise => { + const permission = await Permissions.findOneById(pid); + if (!permission) { + return; + } -export const notifyOnPbxEventChangedById = withDbWatcherCheck( - async (id: T['_id'], clientAction: ClientAction = 'updated'): Promise => { - const item = await PbxEvents.findOneById(id); - if (!item) { - return; - } + return notifyOnPermissionChanged(permission, clientAction); +}; - void api.broadcast('watch.pbxevents', { clientAction, id, data: item }); - }, -); +export const notifyOnRoleChanged = async (role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise => { + void api.broadcast('watch.roles', { clientAction, role }); +}; -export const notifyOnRoleChanged = withDbWatcherCheck( - async (role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise => { - void api.broadcast('watch.roles', { clientAction, role }); - }, -); +export const notifyOnRoleChangedById = async ( + id: T['_id'], + clientAction: 'removed' | 'changed' = 'changed', +): Promise => { + const role = await Roles.findOneById(id); + if (!role) { + return; + } -export const notifyOnRoleChangedById = withDbWatcherCheck( - async (id: T['_id'], clientAction: 'removed' | 'changed' = 'changed'): Promise => { - const role = await Roles.findOneById(id); - if (!role) { - return; - } + void notifyOnRoleChanged(role, clientAction); +}; - void notifyOnRoleChanged(role, clientAction); - }, -); +export const notifyOnLoginServiceConfigurationChanged = async ( + service: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise => { + void api.broadcast('watch.loginServiceConfiguration', { + clientAction, + id: service._id, + data: service, + }); +}; -export const notifyOnLoginServiceConfigurationChanged = withDbWatcherCheck( - async ( - service: Partial & Pick, - clientAction: ClientAction = 'updated', - ): Promise => { - void api.broadcast('watch.loginServiceConfiguration', { - clientAction, - id: service._id, - data: service, - }); - }, -); +export const notifyOnLoginServiceConfigurationChangedByService = async ( + service: T['service'], + clientAction: ClientAction = 'updated', +): Promise => { + const item = await LoginServiceConfiguration.findOneByService>(service, { + projection: { secret: 0 }, + }); + if (!item) { + return; + } -export const notifyOnLoginServiceConfigurationChangedByService = withDbWatcherCheck( - async (service: T['service'], clientAction: ClientAction = 'updated'): Promise => { - const item = await LoginServiceConfiguration.findOneByService>(service, { - projection: { secret: 0 }, - }); - if (!item) { - return; - } + void notifyOnLoginServiceConfigurationChanged(item, clientAction); +}; - void notifyOnLoginServiceConfigurationChanged(item, clientAction); - }, -); +export const notifyOnIntegrationChanged = async ( + data: T, + clientAction: ClientAction = 'updated', +): Promise => { + void api.broadcast('watch.integrations', { clientAction, id: data._id, data }); +}; -export const notifyOnIntegrationChanged = withDbWatcherCheck( - async (data: T, clientAction: ClientAction = 'updated'): Promise => { - void api.broadcast('watch.integrations', { clientAction, id: data._id, data }); - }, -); +export const notifyOnIntegrationChangedById = async ( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise => { + const item = await Integrations.findOneById(id); + if (!item) { + return; + } -export const notifyOnIntegrationChangedById = withDbWatcherCheck( - async (id: T['_id'], clientAction: ClientAction = 'updated'): Promise => { - const item = await Integrations.findOneById(id); - if (!item) { - return; - } + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); +}; - void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); - }, -); +export const notifyOnIntegrationChangedByUserId = async ( + id: T['userId'], + clientAction: ClientAction = 'updated', +): Promise => { + const items = Integrations.findByUserId(id); -export const notifyOnIntegrationChangedByUserId = withDbWatcherCheck( - async (id: T['userId'], clientAction: ClientAction = 'updated'): Promise => { - const items = Integrations.findByUserId(id); + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +}; - for await (const item of items) { - void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); - } - }, -); +export const notifyOnIntegrationChangedByChannels = async ( + channels: T['channel'], + clientAction: ClientAction = 'updated', +): Promise => { + const items = Integrations.findByChannels(channels); -export const notifyOnIntegrationChangedByChannels = withDbWatcherCheck( - async (channels: T['channel'], clientAction: ClientAction = 'updated'): Promise => { - const items = Integrations.findByChannels(channels); + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +}; - for await (const item of items) { - void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); - } - }, -); +export const notifyOnEmailInboxChanged = async ( + data: Pick | T, // TODO: improve typing + clientAction: ClientAction = 'updated', +): Promise => { + void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data }); +}; -export const notifyOnEmailInboxChanged = withDbWatcherCheck( - async ( - data: Pick | T, // TODO: improve typing - clientAction: ClientAction = 'updated', - ): Promise => { - void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data }); - }, -); +export const notifyOnLivechatInquiryChanged = async ( + data: ILivechatInquiryRecord | ILivechatInquiryRecord[], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise => { + const items = Array.isArray(data) ? data : [data]; -export const notifyOnLivechatInquiryChanged = withDbWatcherCheck( - async ( - data: ILivechatInquiryRecord | ILivechatInquiryRecord[], - clientAction: ClientAction = 'updated', - diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, - ): Promise => { - const items = Array.isArray(data) ? data : [data]; + for (const item of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff }); + } +}; - for (const item of items) { - void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff }); - } - }, -); +export const notifyOnLivechatInquiryChangedById = async ( + ids: ILivechatInquiryRecord['_id'] | ILivechatInquiryRecord['_id'][], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise => { + const eligibleIds = Array.isArray(ids) ? ids : [ids]; -export const notifyOnLivechatInquiryChangedById = withDbWatcherCheck( - async ( - ids: ILivechatInquiryRecord['_id'] | ILivechatInquiryRecord['_id'][], - clientAction: ClientAction = 'updated', - diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, - ): Promise => { - const eligibleIds = Array.isArray(ids) ? ids : [ids]; + const items = + clientAction === 'removed' + ? LivechatInquiry.trashFind({ _id: { $in: eligibleIds } }) + : LivechatInquiry.find({ _id: { $in: eligibleIds } }); - const items = - clientAction === 'removed' - ? LivechatInquiry.trashFind({ _id: { $in: eligibleIds } }) - : LivechatInquiry.find({ _id: { $in: eligibleIds } }); + if (!items) { + return; + } - if (!items) { - return; - } + for await (const inquiry of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + } +}; - for await (const inquiry of items) { - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); - } - }, -); +export const notifyOnLivechatInquiryChangedByVisitorIds = async ( + visitorIds: ILivechatInquiryRecord['v']['_id'][], + clientAction: Exclude = 'updated', + diff?: Partial & { queuedAt: Date; takenAt: Date }>, +): Promise => { + const cursor = LivechatInquiry.findByVisitorIds(visitorIds); -export const notifyOnLivechatInquiryChangedByVisitorIds = withDbWatcherCheck( - async ( - visitorIds: ILivechatInquiryRecord['v']['_id'][], - clientAction: Exclude = 'updated', - diff?: Partial & { queuedAt: Date; takenAt: Date }>, - ): Promise => { - const cursor = LivechatInquiry.findByVisitorIds(visitorIds); - - void cursor.forEach((inquiry) => { - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); - }); - }, -); + void cursor.forEach((inquiry) => { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + }); +}; -export const notifyOnLivechatInquiryChangedByRoom = withDbWatcherCheck( - async ( - rids: ILivechatInquiryRecord['rid'] | ILivechatInquiryRecord['rid'][], - clientAction: ClientAction = 'updated', - diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, - ): Promise => { - const eligibleIds = Array.isArray(rids) ? rids : [rids]; +export const notifyOnLivechatInquiryChangedByRoom = async ( + rids: ILivechatInquiryRecord['rid'] | ILivechatInquiryRecord['rid'][], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise => { + const eligibleIds = Array.isArray(rids) ? rids : [rids]; - const items = - clientAction === 'removed' - ? LivechatInquiry.trashFind({ rid: { $in: eligibleIds } }) - : LivechatInquiry.find({ rid: { $in: eligibleIds } }); + const items = + clientAction === 'removed' + ? LivechatInquiry.trashFind({ rid: { $in: eligibleIds } }) + : LivechatInquiry.find({ rid: { $in: eligibleIds } }); - if (!items) { - return; - } + if (!items) { + return; + } - for await (const inquiry of items) { - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); - } - }, -); + for await (const inquiry of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + } +}; -export const notifyOnLivechatInquiryChangedByToken = withDbWatcherCheck( - async ( - token: ILivechatInquiryRecord['v']['token'], - clientAction: ClientAction = 'updated', - diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, - ): Promise => { - const inquiry = await LivechatInquiry.findOneByToken(token); +export const notifyOnLivechatInquiryChangedByToken = async ( + token: ILivechatInquiryRecord['v']['token'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise => { + const inquiry = await LivechatInquiry.findOneByToken(token); - if (!inquiry) { - return; - } + if (!inquiry) { + return; + } - void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); - }, -); + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +}; -export const notifyOnIntegrationHistoryChanged = withDbWatcherCheck( - async ( - data: AtLeast, - clientAction: ClientAction = 'updated', - diff: Partial = {}, - ): Promise => { - void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff }); - }, -); +export const notifyOnIntegrationHistoryChanged = async ( + data: AtLeast, + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise => { + void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff }); +}; -export const notifyOnIntegrationHistoryChangedById = withDbWatcherCheck( - async (id: T['_id'], clientAction: ClientAction = 'updated', diff: Partial = {}): Promise => { - const item = await IntegrationHistory.findOneById(id); +export const notifyOnIntegrationHistoryChangedById = async ( + id: T['_id'], + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise => { + const item = await IntegrationHistory.findOneById(id); - if (!item) { - return; - } + if (!item) { + return; + } - void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff }); - }, -); + void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff }); +}; -export const notifyOnLivechatDepartmentAgentChanged = withDbWatcherCheck( - async ( - data: Partial & Pick, - clientAction: ClientAction = 'updated', - ): Promise => { - void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data }); - }, -); +export const notifyOnLivechatDepartmentAgentChanged = async ( + data: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise => { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data }); +}; -export const notifyOnLivechatDepartmentAgentChangedByDepartmentId = withDbWatcherCheck( - async ( - departmentId: T['departmentId'], - clientAction: 'inserted' | 'updated' = 'updated', - ): Promise => { - const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } }); +export const notifyOnLivechatDepartmentAgentChangedByDepartmentId = async ( + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise => { + const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } }); - for await (const item of items) { - void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); - } - }, -); + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +}; -export const notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId = withDbWatcherCheck( - async ( - agentsIds: T['agentId'][], - departmentId: T['departmentId'], - clientAction: 'inserted' | 'updated' = 'updated', - ): Promise => { - const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, { - projection: { _id: 1, agentId: 1, departmentId: 1 }, - }); - - for await (const item of items) { - void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); - } - }, -); +export const notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId = async ( + agentsIds: T['agentId'][], + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise => { + const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, { + projection: { _id: 1, agentId: 1, departmentId: 1 }, + }); + + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +}; -export const notifyOnSettingChanged = withDbWatcherCheck( - async (setting: ISetting & { editor?: ISettingColor['editor'] }, clientAction: ClientAction = 'updated'): Promise => { - void api.broadcast('watch.settings', { clientAction, setting }); - }, -); +export const notifyOnSettingChanged = async ( + setting: ISetting & { editor?: ISettingColor['editor'] }, + clientAction: ClientAction = 'updated', +): Promise => { + void api.broadcast('watch.settings', { clientAction, setting }); +}; -export const notifyOnSettingChangedById = withDbWatcherCheck( - async (id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise => { - const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id); +export const notifyOnSettingChangedById = async (id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise => { + const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id); - if (!item) { - return; - } + if (!item) { + return; + } - void api.broadcast('watch.settings', { clientAction, setting: item }); - }, -); + void api.broadcast('watch.settings', { clientAction, setting: item }); +}; type NotifyUserChange = { id: IUser['_id']; @@ -410,7 +374,7 @@ type NotifyUserChange = { unset?: Record; }; -export const notifyOnUserChange = withDbWatcherCheck(async ({ clientAction, id, data, diff, unset }: NotifyUserChange) => { +export const notifyOnUserChange = async ({ clientAction, id, data, diff, unset }: NotifyUserChange) => { if (clientAction === 'removed') { void api.broadcast('watch.users', { clientAction, id }); return; @@ -422,12 +386,12 @@ export const notifyOnUserChange = withDbWatcherCheck(async ({ clientAction, id, } void api.broadcast('watch.users', { clientAction, diff: diff!, unset: unset || {}, id }); -}); +}; /** * Calls the callback only if DB Watchers are disabled */ -export const notifyOnUserChangeAsync = withDbWatcherCheck(async (cb: () => Promise) => { +export const notifyOnUserChangeAsync = async (cb: () => Promise) => { const result = await cb(); if (!result) { return; @@ -439,19 +403,23 @@ export const notifyOnUserChangeAsync = withDbWatcherCheck(async (cb: () => Promi } return notifyOnUserChange(result); -}); +}; // TODO this may be only useful on 'inserted' -export const notifyOnUserChangeById = withDbWatcherCheck( - async ({ clientAction, id }: { id: IUser['_id']; clientAction: 'inserted' | 'removed' | 'updated' }) => { - const user = await Users.findOneById(id); - if (!user) { - return; - } +export const notifyOnUserChangeById = async ({ + clientAction, + id, +}: { + id: IUser['_id']; + clientAction: 'inserted' | 'removed' | 'updated'; +}) => { + const user = await Users.findOneById(id); + if (!user) { + return; + } - void notifyOnUserChange({ id, clientAction, data: user }); - }, -); + void notifyOnUserChange({ id, clientAction, data: user }); +}; const getUserNameCached = mem( async (userId: string): Promise => { @@ -504,139 +472,132 @@ export async function getMessageToBroadcast({ id, data }: { id: IMessage['_id']; return message; } -export const notifyOnMessageChange = withDbWatcherCheck(async ({ id, data }: { id: IMessage['_id']; data?: IMessage }): Promise => { +export const notifyOnMessageChange = async ({ id, data }: { id: IMessage['_id']; data?: IMessage }): Promise => { const message = await getMessageToBroadcast({ id, data }); if (!message) { return; } void api.broadcast('watch.messages', { message }); -}); +}; -export const notifyOnSubscriptionChanged = withDbWatcherCheck( - async (subscription: ISubscription, clientAction: ClientAction = 'updated'): Promise => { +export const notifyOnSubscriptionChanged = async (subscription: ISubscription, clientAction: ClientAction = 'updated'): Promise => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); +}; + +export const notifyOnSubscriptionChangedByRoomIdAndUserId = async ( + rid: ISubscription['rid'], + uid: ISubscription['u']['_id'], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomIds(uid, [rid], { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }, -); + }); +}; -export const notifyOnSubscriptionChangedByRoomIdAndUserId = withDbWatcherCheck( - async ( - rid: ISubscription['rid'], - uid: ISubscription['u']['_id'], - clientAction: Exclude = 'updated', - ): Promise => { - const cursor = Subscriptions.findByUserIdAndRoomIds(uid, [rid], { projection: subscriptionFields }); - - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedById = async ( + id: ISubscription['_id'], + clientAction: Exclude = 'updated', +): Promise => { + const subscription = await Subscriptions.findOneById(id); + if (!subscription) { + return; + } -export const notifyOnSubscriptionChangedById = withDbWatcherCheck( - async (id: ISubscription['_id'], clientAction: Exclude = 'updated'): Promise => { - const subscription = await Subscriptions.findOneById(id); - if (!subscription) { - return; - } + void api.broadcast('watch.subscriptions', { clientAction, subscription }); +}; +export const notifyOnSubscriptionChangedByUserPreferences = async ( + uid: ISubscription['u']['_id'], + notificationOriginField: keyof ISubscription, + originFieldNotEqualValue: 'user' | 'subscription', + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByUserPreferences(uid, notificationOriginField, originFieldNotEqualValue, { + projection: subscriptionFields, + }); + + void cursor.forEach((subscription) => { void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }, -); + }); +}; -export const notifyOnSubscriptionChangedByUserPreferences = withDbWatcherCheck( - async ( - uid: ISubscription['u']['_id'], - notificationOriginField: keyof ISubscription, - originFieldNotEqualValue: 'user' | 'subscription', - clientAction: Exclude = 'updated', - ): Promise => { - const cursor = Subscriptions.findByUserPreferences(uid, notificationOriginField, originFieldNotEqualValue, { - projection: subscriptionFields, - }); - - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedByRoomId = async ( + rid: ISubscription['rid'], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByRoomId(rid, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; -export const notifyOnSubscriptionChangedByRoomId = withDbWatcherCheck( - async (rid: ISubscription['rid'], clientAction: Exclude = 'updated'): Promise => { - const cursor = Subscriptions.findByRoomId(rid, { projection: subscriptionFields }); +export const notifyOnSubscriptionChangedByAutoTranslateAndUserId = async ( + uid: ISubscription['u']['_id'], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByAutoTranslateAndUserId(uid, true, { projection: subscriptionFields }); - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; -export const notifyOnSubscriptionChangedByAutoTranslateAndUserId = withDbWatcherCheck( - async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { - const cursor = Subscriptions.findByAutoTranslateAndUserId(uid, true, { projection: subscriptionFields }); +export const notifyOnSubscriptionChangedByUserIdAndRoomType = async ( + uid: ISubscription['u']['_id'], + t: ISubscription['t'], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomType(uid, t, { projection: subscriptionFields }); - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; -export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck( - async ( - uid: ISubscription['u']['_id'], - t: ISubscription['t'], - clientAction: Exclude = 'updated', - ): Promise => { - const cursor = Subscriptions.findByUserIdAndRoomType(uid, t, { projection: subscriptionFields }); - - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedByVisitorIds = async ( + visitorIds: Exclude['_id'][], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findOpenByVisitorIds(visitorIds, { projection: subscriptionFields }); -export const notifyOnSubscriptionChangedByVisitorIds = withDbWatcherCheck( - async ( - visitorIds: Exclude['_id'][], - clientAction: Exclude = 'updated', - ): Promise => { - const cursor = Subscriptions.findOpenByVisitorIds(visitorIds, { projection: subscriptionFields }); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedByNameAndRoomType = async ( + filter: Partial>, + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields }); -export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck( - async (filter: Partial>, clientAction: Exclude = 'updated'): Promise => { - const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields }); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedByUserId = async ( + uid: ISubscription['u']['_id'], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByUserId(uid, { projection: subscriptionFields }); -export const notifyOnSubscriptionChangedByUserId = withDbWatcherCheck( - async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { - const cursor = Subscriptions.findByUserId(uid, { projection: subscriptionFields }); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); +export const notifyOnSubscriptionChangedByRoomIdAndUserIds = async ( + rid: ISubscription['rid'], + uids: ISubscription['u']['_id'][], + clientAction: Exclude = 'updated', +): Promise => { + const cursor = Subscriptions.findByRoomIdAndUserIds(rid, uids, { projection: subscriptionFields }); -export const notifyOnSubscriptionChangedByRoomIdAndUserIds = withDbWatcherCheck( - async ( - rid: ISubscription['rid'], - uids: ISubscription['u']['_id'][], - clientAction: Exclude = 'updated', - ): Promise => { - const cursor = Subscriptions.findByRoomIdAndUserIds(rid, uids, { projection: subscriptionFields }); - - void cursor.forEach((subscription) => { - void api.broadcast('watch.subscriptions', { clientAction, subscription }); - }); - }, -); + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); +}; diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index ddfa2ed011c0e..80cf172c9dcb1 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -9,7 +9,7 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserId, notifyOnSubscriptionChangedByRoomIdAndUserIds, } from './notifyListener'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index d83c7445b4d72..0a347c7526abc 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -13,7 +13,7 @@ import moment from 'moment'; import type { RootFilterOperators } from 'mongodb'; import { getMentions } from './notifyUsersOnMessage'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Notification } from '../../../notification-queue/server/NotificationQueue'; diff --git a/apps/meteor/app/lib/server/lib/validateEmailDomain.js b/apps/meteor/app/lib/server/lib/validateEmailDomain.js index 898a3e6b8942e..d00a8808e8aee 100644 --- a/apps/meteor/app/lib/server/lib/validateEmailDomain.js +++ b/apps/meteor/app/lib/server/lib/validateEmailDomain.js @@ -1,10 +1,10 @@ import dns from 'dns'; import util from 'util'; +import { validateEmail } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import { emailDomainDefaultBlackList } from './defaultBlockedDomainsList'; -import { validateEmail } from '../../../../lib/emailValidator'; import { settings } from '../../../settings/server'; const dnsResolveMx = util.promisify(dns.resolveMx); diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index c5fd718d6911f..0ceda8ab359e9 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -1,13 +1,11 @@ import { api } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { isRoomNativeFederated, type IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { validateFederatedUsername } from '@rocket.chat/federation-matrix'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { beforeAddUsersToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; +import { beforeAddUsersToRoom } from '../../../../server/lib/callbacks/beforeAddUserToRoom'; import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../functions/addUserToRoom'; @@ -54,8 +52,7 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; }); const userInRoom = subscription != null; - // Can't add to direct room ever - if (room.t === 'd') { + if (room.t === 'd' && !isRoomNativeFederated(room)) { throw new Meteor.Error('error-cant-invite-for-direct-room', "Can't invite user to direct rooms", { method: 'addUsersToRoom', }); @@ -91,13 +88,6 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; data.users.map(async (username) => { const sanitizedUsername = sanitizeUsername(username); - // If it's a federated username format and the room is not federated, throw error immediately - if (validateFederatedUsername(sanitizedUsername) && !isRoomNativeFederated(room)) { - throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', { - method: 'addUsersToRoom', - }); - } - const newUser = await Users.findOneByUsernameIgnoringCase(sanitizedUsername); if (!newUser) { throw new Meteor.Error('error-user-not-found', 'User not found', { @@ -107,19 +97,18 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id); if (!subscription) { - await addUserToRoom(data.rid, newUser, user); - } else { - if (!newUser.username) { - return; - } - void api.broadcast('notify.ephemeralMessage', userId, data.rid, { - msg: i18n.t('Username_is_already_in_here', { - postProcess: 'sprintf', - sprintf: [newUser.username], - lng: user?.language, - }), - }); + return addUserToRoom(data.rid, newUser, user); + } + if (!newUser.username) { + return; } + void api.broadcast('notify.ephemeralMessage', userId, data.rid, { + msg: i18n.t('Username_is_already_in_here', { + postProcess: 'sprintf', + sprintf: [newUser.username], + lng: user?.language, + }), + }); }), ); diff --git a/apps/meteor/app/lib/server/methods/createToken.ts b/apps/meteor/app/lib/server/methods/createToken.ts index 199f8cb21e155..63de5b98f4210 100644 --- a/apps/meteor/app/lib/server/methods/createToken.ts +++ b/apps/meteor/app/lib/server/methods/createToken.ts @@ -1,10 +1,5 @@ -import { User } from '@rocket.chat/core-services'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { MeteorError, User } from '@rocket.chat/core-services'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -13,12 +8,11 @@ declare module '@rocket.chat/ddp-client' { } } -export async function generateAccessToken(callee: string, userId: string) { - if ( - !['yes', 'true'].includes(String(process.env.CREATE_TOKENS_FOR_USERS)) || - (callee !== userId && !(await hasPermissionAsync(callee, 'user-generate-access-token'))) - ) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'createToken' }); +const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env; + +export async function generateAccessToken(userId: string, secret: string) { + if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) { + throw new MeteorError('error-not-authorized', 'Not authorized'); } const token = Accounts._generateStampedLoginToken(); @@ -31,16 +25,3 @@ export async function generateAccessToken(callee: string, userId: string) { authToken: token.token, }; } - -Meteor.methods({ - async createToken(userId) { - methodDeprecationLogger.method('createToken', '8.0.0', '/v1/users.createToken'); - - const callee = Meteor.userId(); - if (!callee) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createToken' }); - } - - return generateAccessToken(callee, userId); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index e0a2a844a75eb..cd2096a82a1a9 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -48,10 +48,6 @@ export const getChannelHistory = async ({ }): Promise => { check(rid, String); - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelHistory' }); - } - if (!fromUserId) { return false; } diff --git a/apps/meteor/app/lib/server/methods/getRoomRoles.ts b/apps/meteor/app/lib/server/methods/getRoomRoles.ts index 050992445cc79..0e271e2d64f94 100644 --- a/apps/meteor/app/lib/server/methods/getRoomRoles.ts +++ b/apps/meteor/app/lib/server/methods/getRoomRoles.ts @@ -1,5 +1,4 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -8,7 +7,6 @@ import type { RoomRoles } from '../../../../server/lib/roles/getRoomRoles'; import { getRoomRoles } from '../../../../server/lib/roles/getRoomRoles'; import { canAccessRoomAsync } from '../../../authorization/server'; import { settings } from '../../../settings/server'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -35,12 +33,3 @@ export const executeGetRoomRoles = async (rid: IRoom['_id'], fromUserId?: string return getRoomRoles(rid); }; - -Meteor.methods({ - async getRoomRoles(rid) { - methodDeprecationLogger.method('getRoomRoles', '8.0.0', 'Use the /v1/room.getRoles endpoint instead'); - const fromUserId = Meteor.userId(); - - return executeGetRoomRoles(rid, fromUserId); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/getUserRoles.ts b/apps/meteor/app/lib/server/methods/getUserRoles.ts deleted file mode 100644 index d9f8939611ac3..0000000000000 --- a/apps/meteor/app/lib/server/methods/getUserRoles.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Authorization } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getUserRoles(): Pick[]; - } -} - -Meteor.methods({ - async getUserRoles() { - methodDeprecationLogger.method( - 'getUserRoles', - '8.0.0', - 'This method is deprecated and will be removed in the future. Use the /v1/roles.getUsersInPublicRoles endpoint instead.', - ); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserRoles' }); - } - - return Authorization.getUsersFromPublicRoles(); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts deleted file mode 100644 index 7907d0e185ce6..0000000000000 --- a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; -import { saveUser } from '../functions/saveUser'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - insertOrUpdateUser(userData: Record): Promise; - } -} - -Meteor.methods({ - insertOrUpdateUser: twoFactorRequired(async (userData) => { - methodDeprecationLogger.method('insertOrUpdateUser', '8.0.0', '/v1/users.create'); - - check(userData, Object); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'insertOrUpdateUser', - }); - } - - return saveUser(userId, userData); - }), -}); diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index 8960936d7c33d..d11050bbb2201 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -16,8 +16,8 @@ Meteor.methods({ async joinRoom(rid, code) { check(rid, String); - const userId = await Meteor.userId(); - if (!userId) { + const user = await Meteor.userAsync(); + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); } @@ -26,6 +26,6 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); } - return Room.join({ room, user: { _id: userId }, ...(code ? { joinCode: code } : {}) }); + return Room.join({ room, user, ...(code ? { joinCode: code } : {}) }); }, }); diff --git a/apps/meteor/app/lib/server/methods/saveCustomFields.ts b/apps/meteor/app/lib/server/methods/saveCustomFields.ts deleted file mode 100644 index be4b4c065fccc..0000000000000 --- a/apps/meteor/app/lib/server/methods/saveCustomFields.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { saveCustomFields } from '../functions/saveCustomFields'; -import { RateLimiter } from '../lib'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - saveCustomFields: (fields: IUser['customFields']) => Promise; - } -} - -Meteor.methods({ - async saveCustomFields(fields = {}) { - methodDeprecationLogger.method('saveCustomFields', '8.0.0', 'Use the endpoint /v1/users.updateOwnBasicInfo instead'); - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'saveCustomFields' }); - } - await saveCustomFields(uid, fields); - }, -}); - -RateLimiter.limitMethod('saveCustomFields', 1, 1000, { - userId() { - return true; - }, -}); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 6aa7b7e21e062..db7a017ee7a01 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -19,7 +19,21 @@ import { settings } from '../../../settings/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { +/** + * + * @param uid + * @param message + * @param extraInfo + * - ts: The timestamp of the message. the message object already has a ts, but this value is validated and only a window of 10 seconds is allowed to be used. this value overrides the message.ts value without validation. + * + * + * @returns + */ +export async function executeSendMessage( + uid: IUser['_id'], + message: AtLeast, + extraInfo?: { ts?: Date; previewUrls?: string[] }, +) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { method: 'sendMessage', @@ -32,7 +46,10 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast 60000) { throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { @@ -40,11 +57,10 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast 10000) { - message.ts = new Date(); } - } else { - message.ts = new Date(); + if (tsDiff > 10000) { + message.ts = now; + } } if (message.msg) { @@ -90,7 +106,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast({ tshow: Match.Maybe(Boolean), ts: Match.Maybe(Date), t: Match.Maybe(String), - otrAck: Match.Maybe(String), bot: Match.Maybe(Object), content: Match.Maybe(Object), e2e: Match.Maybe(String), @@ -148,7 +163,7 @@ Meteor.methods({ } try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, previewUrls)); + return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, { previewUrls })); } catch (error: any) { if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { throw new Meteor.Error(error.error || error.message, error.reason, { diff --git a/apps/meteor/app/lib/server/methods/setAdminStatus.ts b/apps/meteor/app/lib/server/methods/setAdminStatus.ts deleted file mode 100644 index 0ccbe729a81d2..0000000000000 --- a/apps/meteor/app/lib/server/methods/setAdminStatus.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isUserFederated } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { addUserToRole } from '../../../authorization/server/methods/addUserToRole'; -import { removeUserFromRole } from '../../../authorization/server/methods/removeUserFromRole'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - setAdminStatus(userId: string, admin?: boolean): void; - } -} - -Meteor.methods({ - async setAdminStatus(userId, admin) { - methodDeprecationLogger.method('setAdminStatus', '8.0.0', 'Use `/v1/roles.addUserToRole` or `/v1/roles.removeUserFromRole`.'); - - check(userId, String); - check(admin, Match.Optional(Boolean)); - - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setAdminStatus' }); - } - - if ((await hasPermissionAsync(uid, 'assign-admin-role')) !== true) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setAdminStatus' }); - } - - const user = await Users.findOne({ _id: userId }, { projection: { username: 1, federated: 1 } }); - if (!user || isUserFederated(user)) { - throw new Meteor.Error('error-not-allowed', 'Federated Users cant be admins', { method: 'setAdminStatus' }); - } - - if (admin) { - await addUserToRole(uid, 'admin', user?.username); - return; - } - - await removeUserFromRole(uid, 'admin', user?.username); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/setUsername.ts b/apps/meteor/app/lib/server/methods/setUsername.ts deleted file mode 100644 index cc8411ac9a688..0000000000000 --- a/apps/meteor/app/lib/server/methods/setUsername.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { setUsernameWithValidation } from '../functions/setUsername'; -import { RateLimiter } from '../lib'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - setUsername(username: string, param?: { joinDefaultChannelsSilenced?: boolean }): string; - } -} - -Meteor.methods({ - async setUsername(username, param = {}) { - methodDeprecationLogger.method('setUsername', '8.0.0', 'Use the endpoint /v1/users.updateOwnBasicInfo instead'); - check(username, String); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setUsername' }); - } - - await setUsernameWithValidation(userId, username, param.joinDefaultChannelsSilenced); - - return username; - }, -}); - -RateLimiter.limitMethod('setUsername', 1, 1000, { - userId() { - return true; - }, -}); diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 45d1343f35721..fa5f8773bf3ea 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -5,8 +5,8 @@ import { Subscriptions, Users } from '@rocket.chat/models'; import type { ActionsBlock } from '@rocket.chat/ui-kit'; import moment from 'moment'; -import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/livechat/imports/server/rest/businessHours.ts b/apps/meteor/app/livechat/imports/server/rest/businessHours.ts index fc0f6209b423c..11f7a99afd856 100644 --- a/apps/meteor/app/livechat/imports/server/rest/businessHours.ts +++ b/apps/meteor/app/livechat/imports/server/rest/businessHours.ts @@ -1,7 +1,18 @@ -import { isGETBusinessHourParams } from '@rocket.chat/rest-typings'; +import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; +import { + isGETBusinessHourParams, + isPOSTLivechatBusinessHoursSaveParams, + isPOSTLivechatBusinessHoursRemoveParams, + POSTLivechatBusinessHoursRemoveSuccessResponse, + POSTLivechatBusinessHoursSaveSuccessResponse, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, +} from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; +import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; import { findLivechatBusinessHour } from '../../../server/api/lib/businessHours'; +import { businessHourManager } from '../../../server/business-hour'; API.v1.addRoute( 'livechat/business-hour', @@ -16,3 +27,51 @@ API.v1.addRoute( }, }, ); + +const livechatBusinessHoursEndpoints = API.v1 + .post( + 'livechat/business-hours.save', + { + response: { + 200: POSTLivechatBusinessHoursSaveSuccessResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + authRequired: true, + body: isPOSTLivechatBusinessHoursSaveParams, + }, + async function action() { + const params = this.bodyParams; + + // TODO: Remove typecasting after refactoring saveBusinessHour logic with proper type logic. See: CORE-1552 + const result = await businessHourManager.saveBusinessHour(params as unknown as ILivechatBusinessHour); + + return API.v1.success(result); + } + ) + .post( + 'livechat/business-hours.remove', + { + response: { + 200: POSTLivechatBusinessHoursRemoveSuccessResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + authRequired: true, + body: isPOSTLivechatBusinessHoursRemoveParams, + }, + async function action() { + const { _id, type } = this.bodyParams; + + await businessHourManager.removeBusinessHourByIdAndType(_id, type); + + return API.v1.success(); + } + ); + +type LivechatBusinessHoursEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends LivechatBusinessHoursEndpoints {} +} diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index ed29cd5c86c80..4390137e95a28 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -4,8 +4,7 @@ import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, Livechat import { makeFunction } from '@rocket.chat/patch-injection'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../../lib/callbacks'; -import { i18n } from '../../../../../server/lib/i18n'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { normalizeAgent } from '../../lib/Helper'; import { getInitSettings } from '../../lib/settings'; @@ -80,7 +79,6 @@ export async function findOpenRoom(token: string, departmentId?: string, callerI departmentId: 1, servedBy: 1, open: 1, - callStatus: 1, }, }; @@ -148,20 +146,6 @@ export async function settings({ businessUnit = '', userId }: { businessUnit?: s background: initSettings.Livechat_background, hideExpandChat: initSettings.Livechat_hide_expand_chat, actionLinks: { - webrtc: [ - { - actionLinksAlignment: 'flex-start', - i18nLabel: 'Join_call', - label: i18n.t('Join_call'), - method_id: 'joinLivechatWebRTCCall', - }, - { - i18nLabel: 'End_call', - label: i18n.t('End_call'), - method_id: 'endLivechatWebRTCCall', - danger: true, - }, - ], jitsi: [ { icon: 'icon-videocam', i18nLabel: 'Accept' }, { icon: 'icon-cancel', i18nLabel: 'Decline' }, diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index a3e0a65762172..f8b4a5d4acd10 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -2,7 +2,7 @@ import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-ty import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; export async function findRooms({ agents, diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.ts b/apps/meteor/app/livechat/server/api/lib/visitors.ts index 15b5d51b533cb..3caf551358a89 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.ts +++ b/apps/meteor/app/livechat/server/api/lib/visitors.ts @@ -2,7 +2,7 @@ import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser, IVisit import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { diff --git a/apps/meteor/app/livechat/server/api/rest.ts b/apps/meteor/app/livechat/server/api/rest.ts index f9da6690185e4..0689e22fda5d3 100644 --- a/apps/meteor/app/livechat/server/api/rest.ts +++ b/apps/meteor/app/livechat/server/api/rest.ts @@ -7,7 +7,6 @@ import './v1/agent'; import './v1/message'; import './v1/customField'; import './v1/room'; -import './v1/videoCall'; import './v1/transfer'; import './v1/contact'; import './v1/webhooks'; diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 228ada8f464d1..bfce5e98e23ea 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -11,7 +11,7 @@ import { isGETLivechatMessagesParams, } from '@rocket.chat/rest-typings'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { isWidget } from '../../../../api/server/helpers/isWidget'; diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 42a1b5666fe73..d8c751b17c9a1 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -6,13 +6,13 @@ import type { IUser, SelectedAgent, TransferByData, + TransferData, } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models'; +import { LivechatVisitors, Users, LivechatRooms } from '@rocket.chat/models'; import { isLiveChatRoomForwardProps, isPOSTLivechatRoomCloseParams, - isPOSTLivechatRoomTransferParams, isPOSTLivechatRoomSurveyParams, isLiveChatRoomJoinProps, isLiveChatRoomSaveInfoProps, @@ -24,10 +24,12 @@ import { validateBadRequestErrorResponse, validateUnauthorizedErrorResponse, validateForbiddenErrorResponse, + ajv, } from '@rocket.chat/rest-typings'; +import { isPOSTLivechatVisitorDepartmentTransferParams } from '@rocket.chat/rest-typings/src/v1/omnichannel'; import { check } from 'meteor/check'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; @@ -236,43 +238,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'livechat/room.transfer', - { validateParams: isPOSTLivechatRoomTransferParams, deprecation: { version: '7.0.0' } }, - { - async post() { - const { rid, token, department } = this.bodyParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Error('invalid-token'); - } - - let room = await findRoom(token, rid); - if (!room) { - throw new Error('invalid-room'); - } - - // update visited page history to not expire - await Messages.keepHistoryForToken(token); - - const { _id, username, name } = guest; - const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - - if (!(await transfer(room, guest, { departmentId: department, transferredBy }))) { - return API.v1.failure(); - } - - room = await findRoom(token, rid); - if (!room) { - throw new Error('invalid-room'); - } - - return API.v1.success({ room }); - }, - }, -); - API.v1.addRoute( 'livechat/room.survey', { validateParams: isPOSTLivechatRoomSurveyParams }, @@ -365,6 +330,74 @@ API.v1.addRoute( }, ); +const livechatVisitorDepartmentTransfer = API.v1.post( + 'livechat/visitor/department.transfer', + { + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + }, + body: isPOSTLivechatVisitorDepartmentTransferParams, + }, + async function action() { + const { rid, token, department } = this.bodyParams; + + const visitor = await findGuest(token); + if (!visitor) { + return API.v1.failure('invalid-token'); + } + const room = await LivechatRooms.findOneById(rid); + + if (!room || room.t !== 'l') { + return API.v1.failure('error-invalid-room'); + } + + if (!room.open) { + return API.v1.failure('This_conversation_is_already_closed'); + } + + // As this is a visitor endpoint, we should not show the mac limit error + if (!(await Omnichannel.isWithinMACLimit(room))) { + return API.v1.failure('error-transefing-chat'); + } + + const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); + if (!guest) { + return API.v1.failure('error-invalid-visitor'); + } + + const transferredBy = normalizeTransferredByData( + { _id: guest._id, username: guest.username, name: guest.name, userType: 'visitor' }, + room, + ); + + const transferData: TransferData = { transferredBy, departmentId: department }; + + const chatForwardedResult = await transfer(room, guest, transferData); + if (!chatForwardedResult) { + return API.v1.failure('error-transfering-chat'); + } + + return API.v1.success(); + }, +); + +type LivechatAnalyticsEndpoints = ExtractRoutesFromAPI; +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends LivechatAnalyticsEndpoints {} +} + API.v1.addRoute( 'livechat/room.join', { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomJoinProps }, diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts deleted file mode 100644 index ee71543b75aba..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Message, Omnichannel } from '@rocket.chat/core-services'; -import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Messages, Settings, Rooms } from '@rocket.chat/models'; -import { isGETWebRTCCall, isPUTWebRTCCallId } from '@rocket.chat/rest-typings'; - -import { i18n } from '../../../../../server/lib/i18n'; -import { API } from '../../../../api/server'; -import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; -import { notifyOnRoomChangedById, notifyOnSettingChanged } from '../../../../lib/server/lib/notifyListener'; -import { settings as rcSettings } from '../../../../settings/server'; -import { updateCallStatus } from '../../lib/utils'; -import { settings } from '../lib/livechat'; - -API.v1.addRoute( - 'livechat/webrtc.call', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETWebRTCCall }, - { - async get() { - const room = await canSendMessageAsync( - this.queryParams.rid, - { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }, - {}, - ); - if (!room) { - throw new Error('invalid-room'); - } - - if (!(await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom))) { - throw new Error('error-mac-limit-reached'); - } - - const webrtcCallingAllowed = rcSettings.get('WebRTC_Enabled') === true && rcSettings.get('Omnichannel_call_provider') === 'WebRTC'; - if (!webrtcCallingAllowed) { - throw new Error('webRTC calling not enabled'); - } - - const config = await settings(); - if (!config.theme?.actionLinks?.webrtc) { - throw new Error('invalid-livechat-config'); - } - - let { callStatus } = room; - - if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { - const value = await Settings.incrementValueById('WebRTC_Calls_Count', 1, { returnDocument: 'after' }); - if (value) { - void notifyOnSettingChanged(value); - } - - callStatus = 'ringing'; - - (await Rooms.setCallStatusAndCallStartTime(room._id, callStatus)).modifiedCount && void notifyOnRoomChangedById(room._id); - - await Message.saveSystemMessage('livechat_webrtc_video_call', room._id, i18n.t('Join_my_room_to_start_the_video_call'), this.user, { - actionLinks: config.theme.actionLinks.webrtc, - }); - } - - const videoCall = { - rid: room._id, - provider: 'webrtc', - callStatus, - }; - return API.v1.success({ videoCall }); - }, - }, -); - -// TODO: investigate if we can deprecate this functionality -API.v1.addRoute( - 'livechat/webrtc.call/:callId', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTWebRTCCallId }, - { - async put() { - const { callId } = this.urlParams; - const { rid, status } = this.bodyParams; - - const room = await canSendMessageAsync( - rid, - { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }, - {}, - ); - if (!room) { - throw new Error('invalid-room'); - } - - if (!(await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom))) { - throw new Error('error-mac-limit-reached'); - } - - const call = await Messages.findOneById(callId); - if (!call || call.t !== 'livechat_webrtc_video_call') { - throw new Error('invalid-callId'); - } - - await updateCallStatus(callId, rid, status, this.user); - - return API.v1.success({ status }); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 2c17181fc3feb..be6a974db958f 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -4,14 +4,13 @@ import { registerGuest } from '@rocket.chat/omni-core'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; import { setMultipleVisitorCustomFields } from '../../lib/custom-fields'; import { notifyGuestStatusChanged, removeContactsByVisitorId } from '../../lib/guests'; import { livechatLogger } from '../../lib/logger'; import { saveRoomInfo } from '../../lib/rooms'; -import { updateCallStatus } from '../../lib/utils'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; API.v1.addRoute( @@ -183,25 +182,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute('livechat/visitor.callStatus', { - async post() { - check(this.bodyParams, { - token: String, - callStatus: String, - rid: String, - callId: String, - }); - - const { token, callStatus, rid, callId } = this.bodyParams; - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - await updateCallStatus(callId, rid, callStatus, guest); - return API.v1.success({ token, callStatus }); - }, -}); - API.v1.addRoute('livechat/visitor.status', { async post() { check(this.bodyParams, { diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index adc7ebf05ce07..4fe6bff2d0d88 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -6,7 +6,7 @@ import moment from 'moment-timezone'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; import { closeBusinessHour } from './closeBusinessHour'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourLogger } from '../lib/logger'; diff --git a/apps/meteor/app/livechat/server/business-hour/index.ts b/apps/meteor/app/livechat/server/business-hour/index.ts index 8df93c5cbaeeb..95540bb44e9aa 100644 --- a/apps/meteor/app/livechat/server/business-hour/index.ts +++ b/apps/meteor/app/livechat/server/business-hour/index.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { BusinessHourManager } from './BusinessHourManager'; import { DefaultBusinessHour } from './Default'; import { SingleBusinessHourBehavior } from './Single'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; export const businessHourManager = new BusinessHourManager(cronJobs); diff --git a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts index 5dcb9513ec2fc..4687d8ef47dfa 100644 --- a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts +++ b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts @@ -1,6 +1,6 @@ import { LivechatDepartment, Users, LivechatDepartmentAgents, LivechatVisitors } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatDepartmentAgentChanged, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; callbacks.add('livechat.afterAgentRemoved', async ({ agent }) => { @@ -21,7 +21,6 @@ callbacks.add('livechat.afterAgentRemoved', async ({ agent }) => { operator: false, livechat: null, statusLivechat: null, - extension: null, openBusinessHours: null, }, }); diff --git a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts index 311343c4ad01d..8e5a17725cef2 100644 --- a/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts +++ b/apps/meteor/app/livechat/server/hooks/afterSaveOmnichannelMessage.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; callbacks.add( 'afterSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 026eb8224d8c4..95355d708412d 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -1,7 +1,7 @@ import { type IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { afterAgentUserActivated, afterAgentAdded, afterRemoveAgent } from '../lib/hooks'; type IAfterSaveUserProps = { @@ -33,7 +33,7 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - await Users.makeAgentUnavailableAndUnsetExtension(user._id); + await Users.makeAgentUnavailable(user._id); } }; diff --git a/apps/meteor/app/livechat/server/hooks/leadCapture.ts b/apps/meteor/app/livechat/server/hooks/leadCapture.ts index 8d40b23b74492..6516c7d0f1e33 100644 --- a/apps/meteor/app/livechat/server/hooks/leadCapture.ts +++ b/apps/meteor/app/livechat/server/hooks/leadCapture.ts @@ -2,8 +2,8 @@ import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatVisitors } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; function validateMessage(message: IMessage, room: IOmnichannelRoom) { diff --git a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts index 01d3014f1c277..d3422ef33bb43 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.ts @@ -1,7 +1,7 @@ import { isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; callbacks.add( 'afterOmnichannelSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index ca7ca8dfbd820..fbda30475d5c2 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -4,7 +4,7 @@ import type { Updater } from '@rocket.chat/models'; import { LivechatRooms, LivechatContacts, LivechatInquiry } from '@rocket.chat/models'; import moment from 'moment'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { isMessageFromBot } from '../lib/isMessageFromBot'; diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessage.ts b/apps/meteor/app/livechat/server/hooks/offlineMessage.ts index cf0f16af709a5..0496d7da1a9e4 100644 --- a/apps/meteor/app/livechat/server/hooks/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/hooks/offlineMessage.ts @@ -1,4 +1,4 @@ -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { sendRequest } from '../lib/webhooks'; diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts index 7cc8f8e6e9ad1..f15c7d717b8f3 100644 --- a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts +++ b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts @@ -2,7 +2,7 @@ import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatDepartment, Users, Rooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 752bce901d314..ffe339adec77b 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, LivechatDepartment, Messages, LivechatRooms } from '@rocket.chat/models'; import moment from 'moment'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; import type { CloseRoomParams } from '../lib/localTypes'; diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index ce420afa1cb86..eff6229b57552 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -2,7 +2,7 @@ import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket. import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; import { isMessageFromBot } from '../lib/isMessageFromBot'; diff --git a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts index 1925e135a562c..acb4c53927c8d 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts @@ -1,7 +1,7 @@ import { isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatInquiry } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { RoutingManager } from '../lib/RoutingManager'; diff --git a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts index 03dcfdbf81bd2..3b0649b7eb5b3 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts @@ -1,7 +1,7 @@ import { isMessageFromVisitor } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; callbacks.add( 'afterOmnichannelSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts index f0c445a78a788..16fa7b239fea3 100644 --- a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts +++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts @@ -2,7 +2,7 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import type { CloseRoomParams } from '../lib/localTypes'; import { sendTranscript } from '../lib/sendTranscript'; diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts index 26554932a021b..687fabe08b055 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts +++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts @@ -3,7 +3,7 @@ import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Messages } from '@rocket.chat/models'; import type { Response } from '@rocket.chat/server-fetch'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; import { getLivechatRoomGuestInfo } from '../lib/guests'; diff --git a/apps/meteor/app/livechat/server/index.ts b/apps/meteor/app/livechat/server/index.ts index e5707e35ed50a..6b1e5b1636746 100644 --- a/apps/meteor/app/livechat/server/index.ts +++ b/apps/meteor/app/livechat/server/index.ts @@ -14,26 +14,8 @@ import './hooks/saveLastMessageToInquiry'; import './hooks/afterUserActions'; import './hooks/afterAgentRemoved'; import './hooks/afterSaveOmnichannelMessage'; -import './methods/changeLivechatStatus'; -import './methods/closeRoom'; -import './methods/getAnalyticsChartData'; -import './methods/getRoutingConfig'; -import './methods/removeAllClosedRooms'; -import './methods/removeCustomField'; -import './methods/removeRoom'; -import './methods/saveAgentInfo'; -import './methods/saveCustomField'; -import './methods/saveDepartment'; import './methods/sendMessageLivechat'; import './methods/sendFileLivechatMessage'; -import './methods/transfer'; -import './methods/setUpConnection'; -import './methods/takeInquiry'; -import './methods/returnAsInquiry'; -import './methods/sendTranscript'; -import './methods/getFirstRoomMessage'; -import './methods/getTagsList'; -import './methods/getDepartmentForwardRestrictions'; import './lib/QueueManager'; import './lib/RoutingManager'; import './lib/routing/External'; @@ -43,4 +25,3 @@ import './lib/stream/agentStatus'; import './sendMessageBySMS'; import './api'; import './api/rest'; -import './methods/saveBusinessHour'; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index dd901e1db3e04..440b77a934a31 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -31,7 +31,7 @@ import { Users, LivechatContacts, } from '@rocket.chat/models'; -import { removeEmpty } from '@rocket.chat/tools'; +import { removeEmpty, validateEmail as validatorFunc } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; @@ -44,8 +44,7 @@ import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissi import { afterRoomQueued, beforeNewRoom } from './hooks'; import { checkOnlineAgents, getOnlineAgents } from './service-status'; import { saveTransferHistory } from './transfer'; -import { callbacks } from '../../../../lib/callbacks'; -import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendNotification } from '../../../lib/server'; diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index d54afdafbac38..20f7a82e4c664 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -29,7 +29,7 @@ import { allowAgentSkipQueue, } from './Helper'; import { afterTakeInquiry, beforeDelegateAgent } from './hooks'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/livechat/server/lib/closeRoom.ts b/apps/meteor/app/livechat/server/lib/closeRoom.ts index 66cbe70c94846..24770e40af29c 100644 --- a/apps/meteor/app/livechat/server/lib/closeRoom.ts +++ b/apps/meteor/app/livechat/server/lib/closeRoom.ts @@ -9,8 +9,8 @@ import type { ClientSession } from 'mongodb'; import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes'; import { livechatLogger as logger } from './logger'; import { parseTranscriptRequest } from './parseTranscriptRequest'; -import { callbacks } from '../../../../lib/callbacks'; import { client, shouldRetryTransaction } from '../../../../server/database/utils'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatInquiryChanged, notifyOnRoomChanged, diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts b/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts index 551572b24ca45..802002cf1832c 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts @@ -5,7 +5,7 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { getAllowedCustomFields } from './getAllowedCustomFields'; import { validateCustomFields } from './validateCustomFields'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomId, diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts index ea2c0ce69148b..d6a8acc646315 100644 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { updateDepartmentAgents } from './Helper'; import { afterDepartmentArchived, afterDepartmentUnarchived } from './hooks'; import { livechatLogger } from './logger'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnLivechatDepartmentAgentChanged, diff --git a/apps/meteor/app/livechat/server/lib/hooks.ts b/apps/meteor/app/livechat/server/lib/hooks.ts index 2cb2c087ca502..baaf226f21d8c 100644 --- a/apps/meteor/app/livechat/server/lib/hooks.ts +++ b/apps/meteor/app/livechat/server/lib/hooks.ts @@ -15,7 +15,7 @@ import { LivechatContacts, LivechatDepartmentAgents, LivechatVisitors, Users } f import { makeFunction } from '@rocket.chat/patch-injection'; import { setUserStatusLivechat } from './utils'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnLivechatDepartmentAgentChangedByDepartmentId } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { sendToCRM } from '../hooks/sendToCRM'; diff --git a/apps/meteor/app/livechat/server/lib/messages.ts b/apps/meteor/app/livechat/server/lib/messages.ts index 6db7855953d76..e04a3f4ddcd72 100644 --- a/apps/meteor/app/livechat/server/lib/messages.ts +++ b/apps/meteor/app/livechat/server/lib/messages.ts @@ -7,7 +7,7 @@ import { LivechatDepartment, Messages } from '@rocket.chat/models'; import type { ILivechatMessage } from './localTypes'; import { getRoom } from './rooms'; import { showConnecting } from './utils'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { deleteMessage as deleteMessageFunc } from '../../../lib/server/functions/deleteMessage'; import { sendMessage as sendMessageFunc } from '../../../lib/server/functions/sendMessage'; import { updateMessage as updateMessageFunc } from '../../../lib/server/functions/updateMessage'; diff --git a/apps/meteor/app/livechat/server/lib/omni-users.ts b/apps/meteor/app/livechat/server/lib/omni-users.ts index 0d8357e426dbf..28a0678cea959 100644 --- a/apps/meteor/app/livechat/server/lib/omni-users.ts +++ b/apps/meteor/app/livechat/server/lib/omni-users.ts @@ -5,7 +5,7 @@ import { removeEmpty } from '@rocket.chat/tools'; import { updateDepartmentAgents } from './Helper'; import { afterAgentAdded, afterRemoveAgent } from './hooks'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index 486adee979cd6..6a7beca8da944 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -30,8 +30,8 @@ import { getRequiredDepartment } from './departmentsLib'; import { checkDefaultAgentOnNewRoom } from './hooks'; import { livechatLogger } from './logger'; import { saveTransferHistory } from './transfer'; -import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; +import { callbacks } from '../../../../server/lib/callbacks'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { notifyOnLivechatInquiryChangedByRoom, diff --git a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts index ee3385879cc4c..25feaa8de1e11 100644 --- a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts +++ b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts @@ -1,7 +1,7 @@ import type { IRoutingMethod, RoutingMethodConfig, SelectedAgent } from '@rocket.chat/core-typings'; import { LivechatDepartmentAgents, Users } from '@rocket.chat/models'; -import { callbacks } from '../../../../../lib/callbacks'; +import { callbacks } from '../../../../../server/lib/callbacks'; import { settings } from '../../../../settings/server'; import { RoutingManager } from '../RoutingManager'; diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index bcd7227c0e383..ca0148742846d 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -17,7 +17,7 @@ import createDOMPurify from 'dompurify'; import { JSDOM } from 'jsdom'; import moment from 'moment-timezone'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import * as Mailer from '../../../mailer/server/api'; diff --git a/apps/meteor/app/livechat/server/lib/takeInquiry.ts b/apps/meteor/app/livechat/server/lib/takeInquiry.ts index c046768ea92c0..1c7541b276f72 100644 --- a/apps/meteor/app/livechat/server/lib/takeInquiry.ts +++ b/apps/meteor/app/livechat/server/lib/takeInquiry.ts @@ -30,6 +30,10 @@ export const takeInquiry = async ( if (!user) { throw new Meteor.Error('error-agent-status-service-offline', 'Agent status is offline or Omnichannel service is not active', { method: 'livechat:takeInquiry', + ...(process.env.TEST_MODE && { + Livechat_enabled_when_agent_idle: settings.get('Livechat_enabled_when_agent_idle'), + user: await Users.findOneById(userId), + }), }); } diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts index dd03d1e3c9c46..45e5d58e1dc4b 100644 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ b/apps/meteor/app/livechat/server/lib/utils.ts @@ -1,13 +1,11 @@ -import { VideoConf } from '@rocket.chat/core-services'; import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatAgent, ILivechatVisitor, IUser } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; +import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import type { Filter } from 'mongodb'; import { RoutingManager } from './RoutingManager'; import type { AKeyOf } from './localTypes'; -import { callbacks } from '../../../../lib/callbacks'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { businessHourManager } from '../business-hour'; @@ -62,14 +60,3 @@ export async function allowAgentChangeServiceStatus(statusLivechat: ILivechatAge return businessHourManager.allowAgentChangeServiceStatus(agentId); } - -export async function updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); - } -} diff --git a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts deleted file mode 100644 index c52f7a8a2e0b0..0000000000000 --- a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../lib/utils'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:changeLivechatStatus'(params?: { status?: ILivechatAgentStatus; agentId?: string }): unknown; - } -} - -Meteor.methods({ - async 'livechat:changeLivechatStatus'({ status, agentId = Meteor.userId() } = {}) { - methodDeprecationLogger.method('livechat:changeLivechatStatus', '7.0.0', '/v1/livechat/agent.status'); - - const uid = Meteor.userId(); - - if (!uid || !agentId) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:changeLivechatStatus', - }); - } - - const agent = await Users.findOneAgentById(agentId, { - projection: { - status: 1, - statusLivechat: 1, - }, - }); - - if (!agent) { - throw new Meteor.Error('error-not-allowed', 'Invalid Agent Id', { - method: 'livechat:changeLivechatStatus', - }); - } - - if (status && !['available', 'not-available'].includes(status)) { - throw new Meteor.Error('error-not-allowed', 'Invalid Status', { - method: 'livechat:changeLivechatStatus', - }); - } - - const newStatus: ILivechatAgentStatus = - status || - (agent.statusLivechat === ILivechatAgentStatus.AVAILABLE ? ILivechatAgentStatus.NOT_AVAILABLE : ILivechatAgentStatus.AVAILABLE); - - if (newStatus === agent.statusLivechat) { - return; - } - - if (agentId !== uid) { - if (!(await hasPermissionAsync(uid, 'manage-livechat-agents'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:changeLivechatStatus', - }); - } - return setUserStatusLivechat(agentId, newStatus); - } - - if (!(await allowAgentChangeServiceStatus(newStatus, agentId))) { - throw new Meteor.Error('error-business-hours-are-closed', 'Not allowed', { - method: 'livechat:changeLivechatStatus', - }); - } - - return setUserStatusLivechat(agentId, newStatus); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.ts b/apps/meteor/app/livechat/server/methods/closeRoom.ts deleted file mode 100644 index 3af000e1762ec..0000000000000 --- a/apps/meteor/app/livechat/server/methods/closeRoom.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users, LivechatRooms, Subscriptions as SubscriptionRaw } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { closeRoom } from '../lib/closeRoom'; - -type CloseRoomOptions = { - clientAction?: boolean; - tags?: string[]; - emailTranscript?: - | { - sendToVisitor: false; - } - | { - sendToVisitor: true; - requestData: Pick, 'email' | 'subject'>; - }; - generateTranscriptPdf?: boolean; -}; - -type LivechatCloseRoomOptions = Omit & { - emailTranscript?: - | { - sendToVisitor: false; - } - | { - sendToVisitor: true; - requestData: NonNullable; - }; - pdfTranscript?: { - requestedBy: string; - }; -}; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:closeRoom'(roomId: string, comment?: string, options?: CloseRoomOptions): void; - } -} - -Meteor.methods({ - async 'livechat:closeRoom'(roomId: string, comment?: string, options?: CloseRoomOptions) { - methodDeprecationLogger.method('livechat:closeRoom', '7.0.0', '/v1/livechat/room.close'); - - const userId = Meteor.userId(); - if (!userId || !(await hasPermissionAsync(userId, 'close-livechat-room'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:closeRoom', - }); - } - - const room = await LivechatRooms.findOneById(roomId); - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:closeRoom', - }); - } - - const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, userId, { - projection: { - _id: 1, - }, - }); - if (!room.open && subscription) { - await SubscriptionRaw.removeByRoomId(roomId); - return; - } - - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); - } - - const user = await Users.findOneById(userId); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'livechat:closeRoom', - }); - } - - if (!subscription && !(await hasPermissionAsync(userId, 'close-others-livechat-room'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:closeRoom', - }); - } - - await closeRoom({ - user, - room, - comment, - options: resolveOptions(user, options), - }); - }, -}); - -const resolveOptions = ( - user: NonNullable['requestedBy'], - options?: CloseRoomOptions, -): LivechatCloseRoomOptions | undefined => { - if (!options) { - return undefined; - } - - const resolvedOptions: LivechatCloseRoomOptions = { - clientAction: options.clientAction, - tags: options.tags, - }; - - if (options.generateTranscriptPdf) { - resolvedOptions.pdfTranscript = { - requestedBy: user._id, - }; - } - - if (!options?.emailTranscript) { - return resolvedOptions; - } - if (options?.emailTranscript.sendToVisitor === false) { - return { - ...resolvedOptions, - emailTranscript: { - sendToVisitor: false, - }, - }; - } - return { - ...resolvedOptions, - emailTranscript: { - sendToVisitor: true, - requestData: { - ...options.emailTranscript.requestData, - requestedBy: user, - requestedAt: new Date(), - }, - }, - }; -}; diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts b/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts deleted file mode 100644 index 92469a614ddb4..0000000000000 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ChartDataResult } from '@rocket.chat/core-services'; -import { OmnichannelAnalytics } from '@rocket.chat/core-services'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:getAnalyticsChartData'(options: { chartOptions: { name: string } }): ChartDataResult | void; - } -} - -Meteor.methods({ - async 'livechat:getAnalyticsChartData'(options) { - methodDeprecationLogger.method('livechat:getAnalyticsChartData', '8.0.0', '/v1/livechat/analytics/dashboards/charts-data'); - const userId = Meteor.userId(); - if (!userId || !(await hasPermissionAsync(userId, 'view-livechat-manager'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:getAnalyticsChartData', - }); - } - - if (!options.chartOptions?.name) { - return; - } - - const user = await Users.findOneById(userId, { projection: { _id: 1, utcOffset: 1 } }); - - if (!user) { - return; - } - - return OmnichannelAnalytics.getAnalyticsChartData({ ...options, utcOffset: user?.utcOffset, executedBy: userId }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.ts b/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.ts deleted file mode 100644 index 1fb6fbaf974ca..0000000000000 --- a/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../lib/callbacks'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:getDepartmentForwardRestrictions'(departmentId: string): unknown; - } -} - -Meteor.methods({ - async 'livechat:getDepartmentForwardRestrictions'(departmentId) { - methodDeprecationLogger.method('livechat:getDepartmentForwardRestrictions', '7.0.0', 'This functionality is no longer supported'); - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'livechat:getDepartmentForwardRestrictions', - }); - } - - const options = await callbacks.run('livechat.onLoadForwardDepartmentRestrictions', { departmentId }); - const { restrictions } = options; - - return restrictions; - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.ts b/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.ts deleted file mode 100644 index e929e3f39ec07..0000000000000 --- a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatRooms, Messages } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:getFirstRoomMessage'(params: { rid: string }): unknown; - } -} - -Meteor.methods({ - async 'livechat:getFirstRoomMessage'({ rid }) { - const uid = Meteor.userId(); - methodDeprecationLogger.method('livechat:getFirsRoomMessage', '7.0.0', 'This functionality is no longer supported'); - if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:getFirstRoomMessage', - }); - } - - check(rid, String); - - const room = await LivechatRooms.findOneById(rid); - - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room'); - } - - return Messages.findOne({ rid }, { sort: { ts: 1 } }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts b/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts deleted file mode 100644 index 4efeeb23a567d..0000000000000 --- a/apps/meteor/app/livechat/server/methods/getRoutingConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { OmichannelRoutingConfig } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { RoutingManager } from '../lib/RoutingManager'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:getRoutingConfig'(): OmichannelRoutingConfig | undefined; - } -} - -Meteor.methods({ - 'livechat:getRoutingConfig'() { - methodDeprecationLogger.method('livechat:getRoutingConfig', '8.0.0', 'v1/livechat/config/routing'); - return RoutingManager.getConfig(); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/getTagsList.ts b/apps/meteor/app/livechat/server/methods/getTagsList.ts deleted file mode 100644 index 3d6cdbd159789..0000000000000 --- a/apps/meteor/app/livechat/server/methods/getTagsList.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ILivechatTag } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../lib/callbacks'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:getTagsList'(): ILivechatTag[]; - } -} - -Meteor.methods({ - 'livechat:getTagsList'() { - methodDeprecationLogger.method('livechat:getTagsList', '7.0.0', 'This functionality is no longer supported'); - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'livechat:getTagsList', - }); - } - - return callbacks.run('livechat.beforeListTags'); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts deleted file mode 100644 index 632c5f1e1e7e1..0000000000000 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Logger } from '@rocket.chat/logger'; -import { LivechatRooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../lib/callbacks'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { removeOmnichannelRoom } from '../lib/rooms'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:removeAllClosedRooms'(departmentIds?: string[]): Promise; - } -} - -Meteor.methods({ - async 'livechat:removeAllClosedRooms'(departmentIds) { - methodDeprecationLogger.method('livechat:removeAllClosedRooms', '8.0.0', '/v1/livechat/rooms.removeAllClosedRooms'); - const logger = new Logger('livechat:removeAllClosedRooms'); - const user = Meteor.userId(); - - if (!user || !(await hasPermissionAsync(user, 'remove-closed-livechat-rooms'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:removeAllClosedRoom', - }); - } - - // These are not debug logs since we want to know when the action is performed - logger.info(`User ${Meteor.userId()} is removing all closed rooms`); - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: user }); - const promises: Promise[] = []; - await LivechatRooms.findClosedRooms(departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => { - promises.push(removeOmnichannelRoom(_id)); - }); - await Promise.all(promises); - - logger.info(`User ${Meteor.userId()} removed ${promises.length} closed rooms`); - return promises.length; - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/removeCustomField.ts b/apps/meteor/app/livechat/server/methods/removeCustomField.ts deleted file mode 100644 index 2dd9de6bb8a02..0000000000000 --- a/apps/meteor/app/livechat/server/methods/removeCustomField.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatCustomField } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import type { DeleteResult } from 'mongodb'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:removeCustomField'(_id: string): DeleteResult; - } -} - -Meteor.methods({ - async 'livechat:removeCustomField'(_id) { - methodDeprecationLogger.method('livechat:removeCustomField', '8.0.0', '/v1/livechat/custom-fields.delete'); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:removeCustomField', - }); - } - - check(_id, String); - - const customField = await LivechatCustomField.findOneById(_id, { projection: { _id: 1 } }); - if (!customField) { - throw new Meteor.Error('error-invalid-custom-field', 'Custom field not found', { - method: 'livechat:removeCustomField', - }); - } - - return LivechatCustomField.removeById(_id); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/removeRoom.ts b/apps/meteor/app/livechat/server/methods/removeRoom.ts deleted file mode 100644 index bfe38121782ef..0000000000000 --- a/apps/meteor/app/livechat/server/methods/removeRoom.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatRooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { removeOmnichannelRoom } from '../lib/rooms'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:removeRoom'(rid: IRoom['_id']): void; - } -} - -Meteor.methods({ - async 'livechat:removeRoom'(rid) { - methodDeprecationLogger.method('livechat:removeRoom', '8.0.0', '/v1/livechat/rooms.delete'); - const user = Meteor.userId(); - if (!user || !(await hasPermissionAsync(user, 'remove-closed-livechat-room'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeRoom' }); - } - - const room = await LivechatRooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:removeRoom', - }); - } - - if (room.t !== 'l') { - throw new Meteor.Error('error-this-is-not-a-livechat-room', 'This is not a Livechat room', { - method: 'livechat:removeRoom', - }); - } - - if (room.open) { - throw new Meteor.Error('error-room-is-not-closed', 'Room is not closed', { - method: 'livechat:removeRoom', - }); - } - - await removeOmnichannelRoom(rid); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts deleted file mode 100644 index 40a1714955e49..0000000000000 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ILivechatDepartment, IRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatRooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { returnRoomAsInquiry } from '../lib/rooms'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:returnAsInquiry'(rid: IRoom['_id'], departmentID?: ILivechatDepartment['_id']): boolean; - } -} - -Meteor.methods({ - async 'livechat:returnAsInquiry'(rid, departmentId) { - methodDeprecationLogger.method('livechat:returnAsInquiry', '8.0.0', '/v1/livechat/inquiries.returnAsInquiry'); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:returnAsInquiry', - }); - } - - const room = await LivechatRooms.findOneById(rid); - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:returnAsInquiry', - }); - } - - return returnRoomAsInquiry(room, departmentId); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts b/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts deleted file mode 100644 index 3e761efa352b6..0000000000000 --- a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { saveAgentInfo } from '../lib/omni-users'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:saveAgentInfo'(_id: string, agentData: Record, agentDepartments: string[]): unknown; - } -} - -Meteor.methods({ - async 'livechat:saveAgentInfo'(_id, agentData, agentDepartments) { - methodDeprecationLogger.method('livechat:saveAgentInfo', '8.0.0', '/v1/livechat/agents.saveInfo'); - check(_id, String); - check(agentData, Object); - check(agentDepartments, [String]); - - const user = await Meteor.userAsync(); - if (!user || !(await hasPermissionAsync(user._id, 'manage-livechat-agents'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:saveAgentInfo', - }); - } - - if (!(await hasRoleAsync(_id, 'livechat-agent'))) { - throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent', { - method: 'livechat:saveAgentInfo', - }); - } - - return saveAgentInfo(_id, agentData, agentDepartments); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts b/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts index 9bf32697fae9a..e69de29bb2d1d 100644 --- a/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts +++ b/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts @@ -1,22 +0,0 @@ -import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { businessHourManager } from '../business-hour'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:saveBusinessHour'(businessHourData: ILivechatBusinessHour): void; - } -} - -Meteor.methods({ - async 'livechat:saveBusinessHour'(businessHourData) { - try { - await businessHourManager.saveBusinessHour(businessHourData); - } catch (e) { - throw new Meteor.Error(e instanceof Error ? e.message : String(e)); - } - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/saveCustomField.ts b/apps/meteor/app/livechat/server/methods/saveCustomField.ts deleted file mode 100644 index abca938fb9e78..0000000000000 --- a/apps/meteor/app/livechat/server/methods/saveCustomField.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { ILivechatCustomField } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatCustomField } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:saveCustomField'( - _id: string, - customFieldData: { - field: string; - label: string; - scope: 'visitor' | 'room'; - visibility: string; - regexp: string; - searchable: boolean; - }, - ): ILivechatCustomField; - } -} - -Meteor.methods({ - async 'livechat:saveCustomField'(_id, customFieldData) { - methodDeprecationLogger.method('livechat:saveCustomField', '8.0.0', '/v1/livechat/custom-fields.save'); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:saveCustomField', - }); - } - - if (_id) { - check(_id, String); - } - - check( - customFieldData, - Match.ObjectIncluding({ - field: String, - label: String, - scope: String, - visibility: String, - regexp: String, - searchable: Boolean, - }), - ); - - if (!/^[0-9a-zA-Z-_]+$/.test(customFieldData.field)) { - throw new Meteor.Error( - 'error-invalid-custom-field-name', - 'Invalid custom field name. Use only letters, numbers, hyphens and underscores.', - { method: 'livechat:saveCustomField' }, - ); - } - - if (_id) { - const customField = await LivechatCustomField.findOneById(_id); - if (!customField) { - throw new Meteor.Error('error-invalid-custom-field', 'Custom Field Not found', { - method: 'livechat:saveCustomField', - }); - } - } - - if (!_id) { - const customField = await LivechatCustomField.findOneById(customFieldData.field); - if (customField) { - throw new Meteor.Error('error-custom-field-name-already-exists', 'Custom field name already exists', { - method: 'livechat:saveCustomField', - }); - } - } - - const { field, label, scope, visibility, ...extraData } = customFieldData; - return LivechatCustomField.createOrUpdateCustomField(_id, field, label, scope, visibility, { - ...extraData, - }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts deleted file mode 100644 index 6f6a7fc157fcd..0000000000000 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { saveDepartment } from '../lib/departmentsLib'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:saveDepartment': ( - _id: string | null, - departmentData: { - enabled: boolean; - name: string; - description?: string; - showOnRegistration: boolean; - email: string; - showOnOfflineForm: boolean; - requestTagBeforeClosingChat?: boolean; - chatClosingTags?: string[]; - fallbackForwardDepartment?: string; - departmentsAllowedToForward?: string[]; - allowReceiveForwardOffline?: boolean; - }, - departmentAgents?: - | { - agentId: string; - count?: number | undefined; - order?: number | undefined; - }[] - | undefined, - departmentUnit?: { _id?: string }, - ) => ILivechatDepartment; - } -} - -Meteor.methods({ - async 'livechat:saveDepartment'(_id, departmentData, departmentAgents, departmentUnit) { - methodDeprecationLogger.method('livechat:saveDepartment', '8.0.0', '/v1/livechat/department'); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-departments'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:saveDepartment', - }); - } - - return saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts index 7369cd450a6b9..ad71a87881528 100644 --- a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts +++ b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts @@ -5,15 +5,12 @@ import type { VideoAttachmentProps, IUpload, } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; import { sendMessageLivechat } from './sendMessageLivechat'; import { FileUpload } from '../../../file-upload/server'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; interface ISendFileLivechatMessage { roomId: string; @@ -22,18 +19,6 @@ interface ISendFileLivechatMessage { msgData?: { avatar?: string; emoji?: string; alias?: string; groupable?: boolean; msg?: string }; } -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - sendFileLivechatMessage( - roomId: string, - visitorToken: string, - file: IUpload, - msgData?: { avatar?: string; emoji?: string; alias?: string; groupable?: boolean; msg?: string }, - ): boolean; - } -} - export const sendFileLivechatMessage = async ({ roomId, visitorToken, file, msgData = {} }: ISendFileLivechatMessage): Promise => { const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); @@ -110,10 +95,3 @@ export const sendFileLivechatMessage = async ({ roomId, visitorToken, file, msgD return sendMessageLivechat({ message: msg }); }; - -Meteor.methods({ - async sendFileLivechatMessage(roomId, visitorToken, file, msgData = {}) { - methodDeprecationLogger.method('sendFileLivechatMessage', '8.0.0', '/v1/livechat/upload/:rid'); - return sendFileLivechatMessage({ roomId, visitorToken, file, msgData }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/sendTranscript.ts b/apps/meteor/app/livechat/server/methods/sendTranscript.ts deleted file mode 100644 index a891ed156c5fd..0000000000000 --- a/apps/meteor/app/livechat/server/methods/sendTranscript.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Omnichannel } from '@rocket.chat/core-services'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatRooms, Users } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RateLimiter } from '../../../lib/server'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { sendTranscript } from '../lib/sendTranscript'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:sendTranscript'(token: string, rid: string, email: string, subject: string): boolean; - } -} - -Meteor.methods({ - async 'livechat:sendTranscript'(token, rid, email, subject) { - methodDeprecationLogger.method('livechat:sendTranscript', '8.0.0', '/v1/livechat/transcript'); - check(rid, String); - check(email, String); - - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'send-omnichannel-chat-transcript'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:sendTranscript', - }); - } - - const user = await Users.findOneById(uid, { - projection: { _id: 1, username: 1, name: 1, utcOffset: 1 }, - }); - - const room = await LivechatRooms.findOneById(rid, { projection: { activity: 1 } }); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:sendTranscript' }); - } - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:sendTranscript' }); - } - - return sendTranscript({ token, rid, email, subject, user }); - }, -}); - -RateLimiter.limitMethod('livechat:sendTranscript', 1, 5000, { - connectionId() { - return true; - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/setUpConnection.ts b/apps/meteor/app/livechat/server/methods/setUpConnection.ts deleted file mode 100644 index 4aea4caa7c6fd..0000000000000 --- a/apps/meteor/app/livechat/server/methods/setUpConnection.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserStatus } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { notifyGuestStatusChanged } from '../lib/guests'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:setUpConnection'(data: { token: string }): void; - } -} - -declare module 'meteor/meteor' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Meteor { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Connection { - livechatToken?: string; - } - } -} - -Meteor.methods({ - 'livechat:setUpConnection'(data) { - methodDeprecationLogger.method('livechat:setUpConnection', '8.0.0', 'This functionality is no longer supported'); - check(data, { - token: String, - }); - - const { token } = data; - - if (this.connection && !this.connection.livechatToken) { - this.connection.livechatToken = token; - this.connection.onClose(async () => { - await notifyGuestStatusChanged(token, UserStatus.OFFLINE); - }); - } - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/takeInquiry.ts b/apps/meteor/app/livechat/server/methods/takeInquiry.ts deleted file mode 100644 index 52336e91d08e3..0000000000000 --- a/apps/meteor/app/livechat/server/methods/takeInquiry.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { takeInquiry } from '../lib/takeInquiry'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:takeInquiry'( - inquiryId: string, - options?: { clientAction: boolean; forwardingToDepartment?: { oldDepartmentId: string; transferData: any } }, - ): unknown; - } -} - -Meteor.methods({ - async 'livechat:takeInquiry'(inquiryId, options) { - methodDeprecationLogger.method('livechat:takeInquiry', '8.0.0', '/v1/livechat/inquiries.take'); - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-not-allowed', 'Invalid User', { - method: 'livechat:takeInquiry', - }); - } - - if (!(await hasPermissionAsync(uid, 'view-l-room'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:takeInquiry', - }); - } - - return takeInquiry(uid, inquiryId, options); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts deleted file mode 100644 index 9d01a944be8ef..0000000000000 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Omnichannel } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatVisitors, LivechatRooms, Subscriptions, Users } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { normalizeTransferredByData } from '../lib/Helper'; -import { transfer } from '../lib/transfer'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'livechat:transfer'(transferData: { - roomId: string; - userId?: string; - departmentId?: string; - comment?: string; - clientAction?: boolean; - }): boolean; - } -} - -// Deprecated in favor of "livechat/room.forward" endpoint -// TODO: Deprecated: Remove in v6.0.0 -Meteor.methods({ - async 'livechat:transfer'(transferData) { - methodDeprecationLogger.method('livechat:transfer', '7.0.0', '/v1/livechat/room.forward'); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' }); - } - - check(transferData, { - roomId: String, - userId: Match.Optional(String), - departmentId: Match.Optional(String), - comment: Match.Optional(String), - clientAction: Match.Optional(Boolean), - }); - - const room = await LivechatRooms.findOneById(transferData.roomId); - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:transfer' }); - } - - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:transfer' }); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:transfer' }); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, { - projection: { _id: 1 }, - }); - if (!subscription && !(await hasPermissionAsync(uid, 'transfer-livechat-guest'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:transfer', - }); - } - - const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); - - if (!guest) { - throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:transfer' }); - } - - const user = await Meteor.userAsync(); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:transfer' }); - } - - const normalizedTransferData: { - roomId: string; - userId?: string; - departmentId?: string; - comment?: string; - clientAction?: boolean; - transferredBy: ReturnType; - transferredTo?: Pick; - } = { - ...transferData, - transferredBy: normalizeTransferredByData(user, room), - }; - - if (normalizedTransferData.userId) { - const userToTransfer = await Users.findOneById(normalizedTransferData.userId); - if (!userToTransfer) { - throw new Meteor.Error('error-invalid-user', 'Invalid user to transfer the room'); - } - normalizedTransferData.transferredTo = { - _id: userToTransfer._id, - username: userToTransfer.username, - name: userToTransfer.name, - }; - } - - return transfer(room, guest, normalizedTransferData); - }, -}); diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts b/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts index a9b29bac2e8f6..2ee80129962a8 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts @@ -7,7 +7,7 @@ import { validators } from './roomAccessValidator.compatibility'; export class AuthorizationLivechat extends ServiceClassInternal implements IAuthorizationLivechat { protected name = 'authorization-livechat'; - protected internal = true; + protected override internal = true; async canAccessRoom(room: IOmnichannelRoom, user?: Pick, extraData?: object): Promise { for await (const validator of validators) { diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index 5454474f09d9f..5b79806951ca1 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -3,7 +3,7 @@ import { isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatVisitors } from '@rocket.chat/models'; import { callbackLogger } from './lib/logger'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { settings } from '../../settings/server'; import { normalizeMessageFileUpload } from '../../utils/server/functions/normalizeMessageFileUpload'; diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index d6035c7ab03fb..73219b781bf4f 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -11,8 +11,8 @@ import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; import { setUserStatusLivechatIf } from './lib/utils'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; -import { callbacks } from '../../../lib/callbacks'; -import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback'; +import { callbacks } from '../../../server/lib/callbacks'; +import { beforeLeaveRoomCallback } from '../../../server/lib/callbacks/beforeLeaveRoomCallback'; import { i18n } from '../../../server/lib/i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { maybeMigrateLivechatRoom } from '../../api/server/lib/maybeMigrateLivechatRoom'; diff --git a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts index b82dcc30411d0..dc112b33c28ae 100644 --- a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts +++ b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts @@ -4,7 +4,7 @@ import { LivechatAgentActivity, Sessions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; const formatDate = (dateTime = new Date()): { date: number } => ({ date: parseInt(moment(dateTime).format('YYYYMMDD')), diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index 90469936b0408..8fc98f884cb5c 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -2,6 +2,7 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import type { ISetting } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; +import { validateEmail } from '@rocket.chat/tools'; import juice from 'juice'; import { Email } from 'meteor/email'; import { Meteor } from 'meteor/meteor'; @@ -9,7 +10,6 @@ import { stripHtml } from 'string-strip-html'; import _ from 'underscore'; import { replaceVariables } from './replaceVariables'; -import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; diff --git a/apps/meteor/app/markdown/server/index.ts b/apps/meteor/app/markdown/server/index.ts index 8cf4e3078e484..4642c5fcbe9a1 100644 --- a/apps/meteor/app/markdown/server/index.ts +++ b/apps/meteor/app/markdown/server/index.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { createMarkdownMessageRenderer, createMarkdownNotificationRenderer } from '../lib/markdown'; export { Markdown } from '../lib/markdown'; diff --git a/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts index 8c836e918a8a4..7f7274803a57a 100644 --- a/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts +++ b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts @@ -1,7 +1,7 @@ import { Team } from '@rocket.chat/core-services'; import type { MessageMention } from '@rocket.chat/core-typings'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { settings } from '../../settings/server'; const beforeGetMentions = async (mentionIds: string[], teamMentions: MessageMention[]): Promise => { diff --git a/apps/meteor/app/message-star/server/starMessage.ts b/apps/meteor/app/message-star/server/starMessage.ts index 96b342f8bfa69..38cfc648af04d 100644 --- a/apps/meteor/app/message-star/server/starMessage.ts +++ b/apps/meteor/app/message-star/server/starMessage.ts @@ -1,11 +1,12 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; @@ -16,7 +17,7 @@ declare module '@rocket.chat/ddp-client' { } } -export const starMessage = async (userId: string, message: Pick & { starred: boolean }): Promise => { +export const starMessage = async (user: IUser, message: Pick & { starred: boolean }): Promise => { if (!settings.get('Message_AllowStarring')) { throw new Meteor.Error('error-action-not-allowed', 'Message starring not allowed', { method: 'starMessage', @@ -24,7 +25,7 @@ export const starMessage = async (userId: string, message: Pick({ async starMessage(message) { - const uid = Meteor.userId(); + methodDeprecationLogger.method('starMessage', '9.0.0', '/v1/chat.starMessage'); + const user = (await Meteor.userAsync()) as IUser; - if (!uid) { + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'starMessage', }); } - return starMessage(uid, message); + return starMessage(user, message); }, }); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 2412feb41afe6..c81230feeb34c 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -1,7 +1,7 @@ import type { ServerResponse } from 'http'; -import type { IUser, IIncomingMessage, IPersonalAccessToken } from '@rocket.chat/core-typings'; -import { CredentialTokens, Rooms, Users } from '@rocket.chat/models'; +import type { IUser, IIncomingMessage, IPersonalAccessToken, IRole } from '@rocket.chat/core-typings'; +import { CredentialTokens, Rooms, Users, Roles } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; @@ -29,6 +29,25 @@ const showErrorMessage = function (res: ServerResponse, err: string): void { res.end(content, 'utf-8'); }; +const convertRoleNamesToIds = async (roleNamesOrIds: string[]): Promise => { + const normalizedRoleNamesOrIds = roleNamesOrIds.map((role) => role.trim()).filter((role) => role.length > 0); + if (!normalizedRoleNamesOrIds.length) { + throw new Error(`No valid role names or ids provided for conversion: ${roleNamesOrIds.join(', ')}`); + } + + const roles = (await Roles.findInIdsOrNames(normalizedRoleNamesOrIds).toArray()).map((role) => role._id); + + if (roles.length !== normalizedRoleNamesOrIds.length) { + SystemLogger.warn(`Failed to convert some role names to ids: ${normalizedRoleNamesOrIds.join(', ')}`); + } + + if (!roles.length) { + throw new Error(`We should have at least one existing role to create the user: ${normalizedRoleNamesOrIds.join(', ')}`); + } + + return roles; +}; + export class SAML { public static async processRequest( req: IIncomingMessage, @@ -79,14 +98,8 @@ export class SAML { } public static async insertOrUpdateSAMLUser(userObject: ISAMLUser): Promise<{ userId: string; token: string }> { - const { - generateUsername, - immutableProperty, - nameOverwrite, - mailOverwrite, - channelsAttributeUpdate, - defaultUserRole = 'user', - } = SAMLUtils.globalSettings; + const { generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate, defaultUserRole } = + SAMLUtils.globalSettings; let customIdentifierMatch = false; let customIdentifierAttributeName: string | null = null; @@ -128,8 +141,14 @@ export class SAML { const active = !settings.get('Accounts_ManuallyApproveNewUsers'); if (!user) { - // If we received any role from the mapping, use them - otherwise use the default role for creation. - const roles = userObject.roles?.length ? userObject.roles : ensureArray(defaultUserRole.split(',')); + let roleNamesOrIds: string[] = []; + if (userObject.roles && userObject.roles.length > 0) { + roleNamesOrIds = userObject.roles; + } else if (defaultUserRole) { + roleNamesOrIds = ensureArray(defaultUserRole.split(',')); + } + + const roles = roleNamesOrIds.length > 0 ? await convertRoleNamesToIds(roleNamesOrIds) : []; const newUser: Record = { name: fullName, @@ -163,7 +182,11 @@ export class SAML { } } - const userId = await Accounts.insertUserDoc({}, newUser); + // only set skipAuthServiceDefaultRoles if SAML is providing its own roles + // otherwise, leave it as false to fallback to generic auth service default roles + // from Accounts_Registration_AuthenticationServices_Default_Roles + const skipAuthServiceDefaultRoles = roleNamesOrIds.length > 0; + const userId = await Accounts.insertUserDoc({ skipAuthServiceDefaultRoles, skipNewUserRolesSetting: true }, newUser); user = await Users.findOneById(userId); if (user && userObject.channels && channelsAttributeUpdate !== true) { @@ -200,7 +223,8 @@ export class SAML { // When updating an user, we only update the roles if we received them from the mapping if (userObject.roles?.length) { - updateData.roles = userObject.roles; + const roles = await convertRoleNamesToIds(userObject.roles); + updateData.roles = roles; } if (userObject.channels && channelsAttributeUpdate === true) { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index f3b875f41a035..8ddb44d45c81b 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -73,6 +73,7 @@ export class SAMLUtils { globalSettings.mailOverwrite = Boolean(samlConfigs.mailOverwrite); globalSettings.channelsAttributeUpdate = Boolean(samlConfigs.channelsAttributeUpdate); globalSettings.includePrivateChannelsInUpdate = Boolean(samlConfigs.includePrivateChannelsInUpdate); + globalSettings.defaultUserRole = samlConfigs.defaultUserRole; if (samlConfigs.immutableProperty && typeof samlConfigs.immutableProperty === 'string') { globalSettings.immutableProperty = samlConfigs.immutableProperty; @@ -82,10 +83,6 @@ export class SAMLUtils { globalSettings.usernameNormalize = samlConfigs.usernameNormalize; } - if (samlConfigs.defaultUserRole && typeof samlConfigs.defaultUserRole === 'string') { - globalSettings.defaultUserRole = samlConfigs.defaultUserRole; - } - if (samlConfigs.userDataFieldMap && typeof samlConfigs.userDataFieldMap === 'string') { globalSettings.userDataFieldMap = samlConfigs.userDataFieldMap; } diff --git a/apps/meteor/app/nextcloud/server/addWebdavServer.ts b/apps/meteor/app/nextcloud/server/addWebdavServer.ts index f53ff59d76ef6..d439389299ae2 100644 --- a/apps/meteor/app/nextcloud/server/addWebdavServer.ts +++ b/apps/meteor/app/nextcloud/server/addWebdavServer.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings/server'; import { addWebdavAccountByToken } from '../../webdav/server/methods/addWebdavAccount'; diff --git a/apps/meteor/app/notifications/server/lib/Presence.ts b/apps/meteor/app/notifications/server/lib/Presence.ts index 17f35a9d39ff1..7e147dfec9ca4 100644 --- a/apps/meteor/app/notifications/server/lib/Presence.ts +++ b/apps/meteor/app/notifications/server/lib/Presence.ts @@ -73,7 +73,7 @@ export class StreamPresence { // eslint-disable-next-line @typescript-eslint/naming-convention static getInstance(Streamer: IStreamerConstructor, name = 'user-presence'): IStreamer<'user-presence'> { return new (class StreamPresence extends Streamer<'user-presence'> { - async _publish( + override async _publish( publication: IPublication, _eventName: string, options: boolean | { useCollection?: boolean; args?: any } = false, diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts index 76e1a8f94b86b..0fb9e2c1e0bf1 100644 --- a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts +++ b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts @@ -5,6 +5,7 @@ import type express from 'express'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; +import { isPlainObject } from '../../../../lib/utils/isPlainObject'; import { OAuth2Server } from '../../../../server/oauth2-server/oauth'; import { API } from '../../../api/server'; @@ -19,13 +20,18 @@ async function getAccessToken(accessToken: string) { } export async function oAuth2ServerAuth(partialRequest: { - headers: Record; - query: Record; -}): Promise<{ user: IUser } | undefined> { + headers: Record; + query: Record; +}): Promise { const headerToken = partialRequest.headers.authorization?.replace('Bearer ', ''); const queryToken = partialRequest.query.access_token; + const incomingToken = headerToken || queryToken; - const accessToken = await getAccessToken(headerToken || queryToken); + if (!incomingToken) { + return; + } + + const accessToken = await getAccessToken(incomingToken); // If there is no token available or the token has expired, return undefined if (!accessToken || (accessToken.expires != null && accessToken.expires < new Date())) { @@ -38,7 +44,7 @@ export async function oAuth2ServerAuth(partialRequest: { return; } - return { user }; + return user; } oauth2server.app.disable('x-powered-by'); @@ -69,8 +75,11 @@ oauth2server.app.get('/oauth/userinfo', async (req: Request, res: Response) => { }); }); -API.v1.addAuthMethod(async function () { - return oAuth2ServerAuth(this.request); +API.v1.addAuthMethod((routeContext) => { + const headers = Object.fromEntries(routeContext.request.headers.entries()); + const query = (isPlainObject(routeContext.queryParams) ? routeContext.queryParams : {}) as Record; + + return oAuth2ServerAuth({ headers, query }); }); (WebApp.connectHandlers as unknown as ReturnType).use(oauth2server.app); diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts index 1d2bf89bbae81..5feb6b8ff078c 100644 --- a/apps/meteor/app/oembed/server/providers.ts +++ b/apps/meteor/app/oembed/server/providers.ts @@ -1,7 +1,7 @@ import type { OEmbedMeta, OEmbedUrlContent, OEmbedProvider } from '@rocket.chat/core-typings'; import { camelCase } from 'change-case'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings/server'; import { Info } from '../../utils/rocketchat.info'; diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts index d3cdb52c0a86b..4a4b6a8b30fe2 100644 --- a/apps/meteor/app/oembed/server/server.ts +++ b/apps/meteor/app/oembed/server/server.ts @@ -16,8 +16,8 @@ import iconv from 'iconv-lite'; import ipRangeCheck from 'ip-range-check'; import jschardet from 'jschardet'; -import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; +import { callbacks } from '../../../server/lib/callbacks'; import { settings } from '../../settings/server'; import { Info } from '../../utils/rocketchat.info'; diff --git a/apps/meteor/app/otr/client/OTR.ts b/apps/meteor/app/otr/client/OTR.ts deleted file mode 100644 index 6237b73e25459..0000000000000 --- a/apps/meteor/app/otr/client/OTR.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { OTRRoom } from './OTRRoom'; -import type { IOTR } from '../lib/IOTR'; - -class OTR implements IOTR { - private instancesByRoomId: { [rid: string]: OTRRoom }; - - constructor() { - this.instancesByRoomId = {}; - } - - getInstanceByRoomId(uid: IUser['_id'], rid: IRoom['_id']): OTRRoom | undefined { - if (this.instancesByRoomId[rid]) { - return this.instancesByRoomId[rid]; - } - - const otrRoom = OTRRoom.create(uid, rid); - - if (!otrRoom) { - return undefined; - } - - this.instancesByRoomId[rid] = otrRoom; - return this.instancesByRoomId[rid]; - } - - closeAllInstances(): void { - // Resets state, but doesnt emit events - // Other party should receive event and fire events - Object.values(this.instancesByRoomId).forEach((instance) => { - instance.softReset(); - }); - - this.instancesByRoomId = {}; - } -} - -export default new OTR(); diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts deleted file mode 100644 index b4a6b43dca13d..0000000000000 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ /dev/null @@ -1,466 +0,0 @@ -import type { IRoom, IMessage, IUser, UserPresence } from '@rocket.chat/core-typings'; -import { UserStatus } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { GenericModal, imperativeModal } from '@rocket.chat/ui-client'; -import EJSON from 'ejson'; -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; - -import { Presence } from '../../../client/lib/presence'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { getUser } from '../../../client/lib/user'; -import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; -import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; -import { Messages } from '../../../client/stores'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; -import type { IOnUserStreamData, IOTRAlgorithm, IOTRDecrypt, IOTRRoom } from '../lib/IOTR'; -import { OtrRoomState } from '../lib/OtrRoomState'; -import { otrSystemMessages } from '../lib/constants'; -import { - decryptAES, - deriveBits, - digest, - encryptAES, - exportKey, - generateKeyPair, - importKey, - importKeyRaw, - joinEncryptedData, -} from '../lib/functions'; - -export class OTRRoom implements IOTRRoom { - private _userId: string; - - private _roomId: string; - - private _keyPair: CryptoKeyPair | null; - - private _exportedPublicKey: JsonWebKey; - - private _sessionKey: CryptoKey | null; - - private _userOnlineComputation: Tracker.Computation; - - private peerId: string; - - private state: ReactiveVar = new ReactiveVar(OtrRoomState.NOT_STARTED); - - private isFirstOTR: boolean; - - private onPresenceEventHook: (event: UserPresence | undefined) => void; - - protected constructor(uid: IUser['_id'], rid: IRoom['_id'], peerId: IUser['_id']) { - this._userId = uid; - this._roomId = rid; - this._keyPair = null; - this._sessionKey = null; - this.peerId = peerId; - this.isFirstOTR = true; - this.onPresenceEventHook = this.onPresenceEvent.bind(this); - } - - public static create(uid: IUser['_id'], rid: IRoom['_id']): OTRRoom | undefined { - const peerId = getUidDirectMessage(rid); - - if (!peerId) { - return undefined; - } - - return new OTRRoom(uid, rid, peerId); - } - - getPeerId(): string { - return this.peerId; - } - - getState(): OtrRoomState { - return this.state.get(); - } - - setState(nextState: OtrRoomState): void { - if (this.getState() === nextState) { - return; - } - - this.state.set(nextState); - } - - async handshake(refresh?: boolean): Promise { - this.setState(OtrRoomState.ESTABLISHING); - - await this.generateKeyPair(); - sdk.publish('notify-user', [ - `${this.peerId}/otr`, - 'handshake', - { - roomId: this._roomId, - userId: this._userId, - publicKey: EJSON.stringify(this._exportedPublicKey), - refresh, - }, - ]); - - if (refresh) { - const user = getUser(); - if (!user) return; - - await sdk.rest.post('/v1/chat.otr', { - roomId: this._roomId, - type: otrSystemMessages.USER_REQUESTED_OTR_KEY_REFRESH, - }); - this.isFirstOTR = false; - } - } - - onPresenceEvent(event: UserPresence | undefined): void { - if (!event) { - return; - } - if (event.status !== UserStatus.OFFLINE) { - return; - } - console.warn(`OTR Room ${this._roomId} ended because ${this.peerId} went offline`); - this.end(); - - imperativeModal.open({ - component: GenericModal, - props: { - variant: 'warning', - title: t('OTR'), - children: t('OTR_Session_ended_other_user_went_offline', { username: event.username }), - confirmText: t('Ok'), - onClose: imperativeModal.close, - onConfirm: imperativeModal.close, - }, - }); - } - - // Starts listening to other user's status changes and end OTR if any of the Users goes offline - // this should be called in 2 places: on acknowledge (meaning user accepted OTR) or on establish (meaning user initiated OTR) - listenToUserStatus(): void { - Presence.listen(this.peerId, this.onPresenceEventHook); - } - - acknowledge(): void { - void sdk.rest.post('/v1/statistics.telemetry', { params: [{ eventName: 'otrStats', timestamp: Date.now(), rid: this._roomId }] }); - - sdk.publish('notify-user', [ - `${this.peerId}/otr`, - 'acknowledge', - { - roomId: this._roomId, - userId: this._userId, - publicKey: EJSON.stringify(this._exportedPublicKey), - }, - ]); - } - - deny(): void { - this.reset(); - this.setState(OtrRoomState.DECLINED); - sdk.publish('notify-user', [ - `${this.peerId}/otr`, - 'deny', - { - roomId: this._roomId, - userId: this._userId, - }, - ]); - } - - softReset(): void { - this.isFirstOTR = true; - this.setState(OtrRoomState.NOT_STARTED); - this._keyPair = null; - this._exportedPublicKey = {}; - this._sessionKey = null; - } - - deleteOTRMessages(): void { - Messages.state.remove( - (record) => record.rid === this._roomId && !!record.t && ['otr', 'otr-ack', ...Object.values(otrSystemMessages)].includes(record.t), - ); - } - - end(): void { - this.isFirstOTR = true; - this.reset(); - this.setState(OtrRoomState.NOT_STARTED); - Presence.stop(this.peerId, this.onPresenceEventHook); - this.deleteOTRMessages(); - sdk.publish('notify-user', [ - `${this.peerId}/otr`, - 'end', - { - roomId: this._roomId, - userId: this._userId, - }, - ]); - } - - reset(): void { - this._keyPair = null; - this._exportedPublicKey = {}; - this._sessionKey = null; - void sdk.call('deleteOldOTRMessages', this._roomId); - } - - async generateKeyPair(): Promise { - if (this._userOnlineComputation) { - this._userOnlineComputation.stop(); - } - - this._userOnlineComputation = Tracker.autorun(() => { - const $room = document.querySelector(`#chat-window-${this._roomId}`); - const $title = $room?.querySelector('.rc-header__title'); - if (this.getState() === OtrRoomState.ESTABLISHED) { - if ($room && $title && !$title.querySelector('.otr-icon')) { - $title.prepend(""); - } - } else if ($title) { - $title.querySelector('.otr-icon')?.remove(); - } - }); - try { - // Generate an ephemeral key pair. - this._keyPair = await generateKeyPair(); - - if (!this._keyPair.publicKey) { - throw new Error('Public key is not generated'); - } - - this._exportedPublicKey = await exportKey(this._keyPair.publicKey); - - // Once we have generated new keys, it's safe to delete old messages - void sdk.call('deleteOldOTRMessages', this._roomId); - } catch (e) { - this.setState(OtrRoomState.ERROR); - throw e; - } - } - - async importPublicKey(publicKey: string): Promise { - try { - if (!this._keyPair) throw new Error('No key pair'); - const publicKeyObject: JsonWebKey = EJSON.parse(publicKey); - const peerPublicKey = await importKey(publicKeyObject); - const ecdhObj: IOTRAlgorithm = { - name: 'ECDH', - namedCurve: 'P-256', - public: peerPublicKey, - }; - const bits = await deriveBits({ ecdhObj, _keyPair: this._keyPair }); - const hashedBits = await digest(bits); - // We truncate the hash to 128 bits. - const sessionKeyData = new Uint8Array(hashedBits).slice(0, 16); - // Session key available. - this._sessionKey = await importKeyRaw(sessionKeyData); - } catch (e) { - this.setState(OtrRoomState.ERROR); - throw e; - } - } - - async encryptText(data: string | Uint8Array): Promise { - if (typeof data === 'string') { - data = new TextEncoder().encode( - EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) }), - ) as Uint8Array; - } - try { - if (!this._sessionKey) throw new Error('Session Key not available'); - - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encryptedData = await encryptAES({ iv, _sessionKey: this._sessionKey, data }); - - const output = joinEncryptedData({ encryptedData, iv }); - - return EJSON.stringify(output); - } catch (e) { - this.setState(OtrRoomState.ERROR); - throw new Meteor.Error('encryption-error', 'Encryption error.'); - } - } - - async encrypt(message: Pick): Promise { - try { - const data = new TextEncoder().encode( - EJSON.stringify({ - _id: message._id, - text: message.msg, - userId: this._userId, - ack: Random.id((Random.fraction() + 1) * 20), - ts: new Date(), - }), - ) as Uint8Array; - const enc = await this.encryptText(data); - return enc; - } catch (e) { - throw new Meteor.Error('encryption-error', 'Encryption error.'); - } - } - - async decrypt(message: string): Promise { - try { - if (!this._sessionKey) throw new Error('Session Key not available.'); - - const cipherText: Uint8Array = EJSON.parse(message); - const data = await decryptAES(cipherText, this._sessionKey); - const msgDecoded: IOTRDecrypt = EJSON.parse(new TextDecoder('UTF-8').decode(new Uint8Array(data))); - if (msgDecoded && typeof msgDecoded === 'object') { - return msgDecoded; - } - return message; - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - this.setState(OtrRoomState.ERROR); - return message; - } - } - - async onUserStream(type: string, data: IOnUserStreamData): Promise { - switch (type) { - case 'handshake': - let timeout: NodeJS.Timeout; - - const establishConnection = async (): Promise => { - this.setState(OtrRoomState.ESTABLISHING); - clearTimeout(timeout); - try { - if (!data.publicKey) throw new Error('Public key is not generated'); - await this.generateKeyPair(); - await this.importPublicKey(data.publicKey); - await goToRoomById(data.roomId); - setTimeout(async () => { - this.setState(OtrRoomState.ESTABLISHED); - this.acknowledge(); - this.listenToUserStatus(); - - if (data.refresh) { - await sdk.rest.post('/v1/chat.otr', { - roomId: this._roomId, - type: otrSystemMessages.USER_KEY_REFRESHED_SUCCESSFULLY, - }); - } - }, 0); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - throw new Meteor.Error('establish-connection-error', 'Establish connection error.'); - } - }; - - const closeOrCancelModal = (): void => { - clearTimeout(timeout); - this.deny(); - imperativeModal.close(); - }; - - try { - const obj = await Presence.get(data.userId); - if (!obj?.username) { - throw new Meteor.Error('user-not-defined', 'User not defined.'); - } - - if (data.refresh && this.getState() === OtrRoomState.ESTABLISHED) { - this.reset(); - await establishConnection(); - } else { - /* We have to check if there's an in progress handshake request because - Notifications.notifyUser will sometimes dispatch 2 events */ - if (this.getState() === OtrRoomState.REQUESTED) { - return; - } - - if (this.getState() === OtrRoomState.ESTABLISHED) { - this.reset(); - } - - this.setState(OtrRoomState.REQUESTED); - imperativeModal.open({ - component: GenericModal, - props: { - variant: 'warning', - title: t('OTR'), - children: t('Username_wants_to_start_otr_Do_you_want_to_accept', { - username: obj.username, - }), - confirmText: t('Yes'), - cancelText: t('No'), - onClose: (): void => closeOrCancelModal(), - onCancel: (): void => closeOrCancelModal(), - onConfirm: async (): Promise => { - await establishConnection(); - imperativeModal.close(); - }, - }, - }); - timeout = setTimeout(() => { - this.setState(OtrRoomState.TIMEOUT); - imperativeModal.close(); - }, 10000); - } - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - break; - - case 'acknowledge': - try { - if (!data.publicKey) throw new Error('Public key is not generated'); - await this.importPublicKey(data.publicKey); - - this.setState(OtrRoomState.ESTABLISHED); - - if (this.isFirstOTR) { - this.listenToUserStatus(); - await sdk.rest.post('/v1/chat.otr', { - roomId: this._roomId, - type: otrSystemMessages.USER_JOINED_OTR, - }); - } - this.isFirstOTR = false; - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - break; - - case 'deny': - if (this.getState() === OtrRoomState.ESTABLISHING) { - this.reset(); - this.setState(OtrRoomState.DECLINED); - } - break; - - case 'end': - try { - const obj = await Presence.get(this.peerId); - if (!obj?.username) { - throw new Meteor.Error('user-not-defined', 'User not defined.'); - } - - if (this.getState() === OtrRoomState.ESTABLISHED) { - this.reset(); - this.setState(OtrRoomState.NOT_STARTED); - this.deleteOTRMessages(); - imperativeModal.open({ - component: GenericModal, - props: { - variant: 'warning', - title: t('OTR'), - children: t('Username_ended_the_OTR_session', { username: obj.username }), - confirmText: t('Ok'), - onClose: imperativeModal.close, - onConfirm: imperativeModal.close, - }, - }); - } - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - - break; - } - } -} diff --git a/apps/meteor/app/otr/client/events.ts b/apps/meteor/app/otr/client/events.ts deleted file mode 100644 index 9ff84c4651570..0000000000000 --- a/apps/meteor/app/otr/client/events.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; - -import OTR from './OTR'; - -Accounts.onLogout(() => { - OTR.closeAllInstances(); -}); diff --git a/apps/meteor/app/otr/client/index.ts b/apps/meteor/app/otr/client/index.ts deleted file mode 100644 index 05aaf3c550f07..0000000000000 --- a/apps/meteor/app/otr/client/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './OTRRoom'; -import './OTR'; -import './events'; diff --git a/apps/meteor/app/otr/lib/IOTR.ts b/apps/meteor/app/otr/lib/IOTR.ts deleted file mode 100644 index c015c0dd425f6..0000000000000 --- a/apps/meteor/app/otr/lib/IOTR.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; - -import type { OtrRoomState } from './OtrRoomState'; -import type { OTRRoom } from '../client/OTRRoom'; - -export interface IOnUserStreamData { - roomId: IRoom['_id']; - userId: IUser['_id']; - publicKey?: string; - refresh?: boolean; -} - -export interface IOTRDecrypt { - ack: string | Uint8Array; - text: string; - ts: Date; - userId: IUser['_id']; - _id: string; -} - -export interface IOTRRoom { - getPeerId(): string; - getState(): OtrRoomState; - setState(nextState: OtrRoomState): void; - handshake(refresh?: boolean): Promise; - acknowledge(): void; - deny(): void; - end(): void; - reset(): void; - generateKeyPair(): Promise; - importPublicKey(publicKey: string): Promise; - encryptText(data: string | Uint8Array): Promise; - encrypt(message: IMessage): Promise; - decrypt(message: string): Promise; - onUserStream(type: string, data: IOnUserStreamData): Promise; -} - -export interface IOTR { - getInstanceByRoomId(userId: IUser['_id'], roomId: IRoom['_id']): OTRRoom | undefined; -} - -export interface IOTRAlgorithm extends EcKeyAlgorithm, EcdhKeyDeriveParams {} diff --git a/apps/meteor/app/otr/lib/OtrRoomState.ts b/apps/meteor/app/otr/lib/OtrRoomState.ts deleted file mode 100644 index c299e5bdb52ac..0000000000000 --- a/apps/meteor/app/otr/lib/OtrRoomState.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum OtrRoomState { - DISABLED = 'DISABLED', - NOT_STARTED = 'NOT_STARTED', - REQUESTED = 'REQUESTED', - ESTABLISHING = 'ESTABLISHING', - ESTABLISHED = 'ESTABLISHED', - ERROR = 'ERROR', - TIMEOUT = 'TIMEOUT', - DECLINED = 'DECLINED', -} diff --git a/apps/meteor/app/otr/lib/constants.ts b/apps/meteor/app/otr/lib/constants.ts deleted file mode 100644 index 0911d9169f117..0000000000000 --- a/apps/meteor/app/otr/lib/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum otrSystemMessages { - USER_JOINED_OTR = 'user_joined_otr', - USER_REQUESTED_OTR_KEY_REFRESH = 'user_requested_otr_key_refresh', - USER_KEY_REFRESHED_SUCCESSFULLY = 'user_key_refreshed_successfully', -} diff --git a/apps/meteor/app/otr/lib/functions.ts b/apps/meteor/app/otr/lib/functions.ts deleted file mode 100644 index 78dfb4b6d4fef..0000000000000 --- a/apps/meteor/app/otr/lib/functions.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { IOTRAlgorithm } from './IOTR'; - -const { subtle } = global.crypto; - -export const joinEncryptedData = ({ encryptedData, iv }: { encryptedData: ArrayBuffer; iv: Uint8Array }): Uint8Array => { - const cipherText = new Uint8Array(encryptedData); - const output = new Uint8Array(iv.length + cipherText.length); - output.set(iv, 0); - output.set(cipherText, iv.length); - return output; -}; -export const encryptAES = async ({ - iv, - _sessionKey, - data, -}: { - iv: Uint8Array; - _sessionKey: CryptoKey; - data: Uint8Array; -}): Promise => - subtle.encrypt( - { - name: 'AES-GCM', - iv, - }, - _sessionKey, - data, - ); -export const digest = async (bits: ArrayBuffer): Promise => - subtle.digest( - { - name: 'SHA-256', - }, - bits, - ); -export const deriveBits = async ({ ecdhObj, _keyPair }: { ecdhObj: IOTRAlgorithm; _keyPair: CryptoKeyPair }): Promise => { - if (!_keyPair.privateKey) { - throw new Error('No private key'); - } - - return subtle.deriveBits(ecdhObj, _keyPair.privateKey, 256); -}; - -export const importKey = async (publicKeyObject: JsonWebKey): Promise => - subtle.importKey( - 'jwk', - publicKeyObject, - { - name: 'ECDH', - namedCurve: 'P-256', - }, - false, - [], - ); -export const importKeyRaw = async (sessionKeyData: Uint8Array): Promise => - subtle.importKey( - 'raw', - sessionKeyData, - { - name: 'AES-GCM', - }, - false, - ['encrypt', 'decrypt'], - ); -export const exportKey = async (_keyPair: CryptoKey): Promise => subtle.exportKey('jwk', _keyPair); -export const generateKeyPair = async (): Promise => - subtle.generateKey( - { - name: 'ECDH', - namedCurve: 'P-256', - }, - false, - ['deriveKey', 'deriveBits'], - ); -export const decryptAES = async (cipherText: Uint8Array, _sessionKey: CryptoKey): Promise => { - const iv = cipherText.slice(0, 12); - cipherText = cipherText.slice(12); - const data = await subtle.decrypt( - { - name: 'AES-GCM', - iv, - }, - _sessionKey, - cipherText, - ); - return data; -}; diff --git a/apps/meteor/app/otr/server/index.ts b/apps/meteor/app/otr/server/index.ts deleted file mode 100644 index 7839b0d0d7a24..0000000000000 --- a/apps/meteor/app/otr/server/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './methods/updateOTRAck'; -import './methods/sendSystemMessages'; -import './methods/deleteOldOTRMessages'; diff --git a/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.ts b/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.ts deleted file mode 100644 index 1b57c65b4bb7d..0000000000000 --- a/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Subscriptions, ReadReceipts } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - deleteOldOTRMessages(roomId: IRoom['_id']): Promise; - } -} - -Meteor.methods({ - async deleteOldOTRMessages(roomId: IRoom['_id']): Promise { - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'deleteOldOTRMessages', - }); - } - - const now = new Date(); - const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); - if (subscription?.t !== 'd') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'deleteOldOTRMessages', - }); - } - - await Messages.deleteOldOTRMessages(roomId, now); - await ReadReceipts.removeOTRReceiptsUntilDate(roomId, now); - }, -}); diff --git a/apps/meteor/app/otr/server/methods/sendSystemMessages.ts b/apps/meteor/app/otr/server/methods/sendSystemMessages.ts deleted file mode 100644 index 13abbd7511afd..0000000000000 --- a/apps/meteor/app/otr/server/methods/sendSystemMessages.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - sendSystemMessages(rid: string, user: string | undefined, id: string): void; - } -} - -Meteor.methods({ - sendSystemMessages() { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendSystemMessages' }); - } - - // deprecated, use REST /v1/chat.otr instead - }, -}); diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts deleted file mode 100644 index 64e5e97fa4e5b..0000000000000 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { IOTRMessage } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - updateOTRAck({ message, ack }: { message: IOTRMessage; ack: string }): void; - } -} - -Meteor.methods({ - async updateOTRAck({ message, ack }) { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); - } - - check(ack, String); - check(message, { - _id: String, - rid: String, - msg: String, - t: String, - ts: Date, - u: { - _id: String, - username: String, - name: String, - }, - }); - - if (message?.t !== 'otr') { - throw new Meteor.Error('error-invalid-message', 'Invalid message type', { method: 'updateOTRAck' }); - } - - const room = await Rooms.findOneByIdAndType(message.rid, 'd', { projection: { t: 1, _id: 1, uids: 1 } }); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateOTRAck' }); - } - - if (!(await canAccessRoomAsync(room, { _id: uid })) || (room.uids && (!message.u._id || !room.uids.includes(message.u._id)))) { - throw new Meteor.Error('error-invalid-user', 'Invalid user, not in room', { method: 'updateOTRAck' }); - } - - const acknowledgeMessage: IOTRMessage = { ...message, otrAck: ack }; - void api.broadcast('otrAckUpdate', { roomId: message.rid, acknowledgeMessage }); - }, -}); diff --git a/apps/meteor/app/push-notifications/server/lib/PushNotification.ts b/apps/meteor/app/push-notifications/server/lib/PushNotification.ts index 3e021cfbcc759..7fd7f404ac970 100644 --- a/apps/meteor/app/push-notifications/server/lib/PushNotification.ts +++ b/apps/meteor/app/push-notifications/server/lib/PushNotification.ts @@ -2,7 +2,7 @@ import type { IMessage, IPushNotificationConfig, IRoom, IUser } from '@rocket.ch import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { RocketChatAssets } from '../../../assets/server'; import { replaceMentionedUsernamesWithFullNames, parseMessageTextPerUser } from '../../../lib/server/functions/notifications'; import { getPushData } from '../../../lib/server/functions/notifications/mobile'; diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 95543aaa43e90..8382214a6b35a 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -1,7 +1,7 @@ import type { IAppsTokens, RequiredField, Optional, IPushNotificationConfig } from '@rocket.chat/core-typings'; import { AppsTokens } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { pick } from '@rocket.chat/tools'; +import { pick, truncateString } from '@rocket.chat/tools'; import Ajv from 'ajv'; import { JWT } from 'google-auth-library'; import { Match, check } from 'meteor/check'; @@ -15,6 +15,9 @@ import { settings } from '../../settings/server'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); +const PUSH_TITLE_LIMIT = 65; +const PUSH_MESSAGE_BODY_LIMIT = 240; + const ajv = new Ajv({ coerceTypes: true, }); @@ -459,8 +462,10 @@ class PushClass { createdBy: '', sent: false, sending: 0, + title: truncateString(options.title, PUSH_TITLE_LIMIT), + text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT), - ...pick(options, 'from', 'title', 'text', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'), + ...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'), ...(this.hasApnOptions(options) ? { diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 7d30963e82cd6..483b1fff8ed7a 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,17 +1,15 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api, Message } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../lib/callbacks'; +import { callbacks } from '../../../server/lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { emoji } from '../../emoji/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; -import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; import { notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; export const removeUserReaction = (message: IMessage, reaction: string, username: string) => { @@ -151,28 +149,3 @@ declare module '@rocket.chat/ddp-client' { setReaction(reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): boolean | undefined; } } - -Meteor.methods({ - async setReaction(reaction, messageId, shouldReact) { - methodDeprecationLogger.method('setReaction', '8.0.0', '/v1/chat.react'); - - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); - } - - try { - await executeSetReaction(uid, reaction, messageId, shouldReact); - } catch (e: any) { - if (e.error === 'error-not-allowed' && e.reason && e.details && e.details.rid) { - void api.broadcast('notify.ephemeralMessage', uid, e.details.rid, { - msg: e.reason, - }); - - return false; - } - - throw e; - } - }, -}); diff --git a/apps/meteor/app/search/server/events/index.ts b/apps/meteor/app/search/server/events/index.ts index fcf583d63f1b7..30ad122312772 100644 --- a/apps/meteor/app/search/server/events/index.ts +++ b/apps/meteor/app/search/server/events/index.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { settings } from '../../../settings/server'; import { searchProviderService } from '../service'; import { EventService } from './EventService'; diff --git a/apps/meteor/app/search/server/search.internalService.ts b/apps/meteor/app/search/server/search.internalService.ts index bf4a0c484ddc0..f5854028a4fbb 100644 --- a/apps/meteor/app/search/server/search.internalService.ts +++ b/apps/meteor/app/search/server/search.internalService.ts @@ -8,7 +8,7 @@ import { settings } from '../../settings/server'; class Search extends ServiceClassInternal { protected name = 'search'; - protected internal = true; + protected override internal = true; constructor() { super(); diff --git a/apps/meteor/app/settings/server/CachedSettings.ts b/apps/meteor/app/settings/server/CachedSettings.ts index 9a42569b4cf64..6a16a4c761313 100644 --- a/apps/meteor/app/settings/server/CachedSettings.ts +++ b/apps/meteor/app/settings/server/CachedSettings.ts @@ -106,7 +106,7 @@ export class CachedSettings * @param _id - The setting id * @returns {boolean} */ - public has(_id: ISetting['_id']): boolean { + public override has(_id: ISetting['_id']): boolean { if (!this.ready && warn) { SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); } diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 70418d407443e..17fce65133090 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -7,8 +7,8 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { rocketLogger } from './logger'; -import { callbacks } from '../../../lib/callbacks'; import { sleep } from '../../../lib/utils/sleep'; +import { callbacks } from '../../../server/lib/callbacks'; import { createRoom } from '../../lib/server/functions/createRoom'; import { sendMessage } from '../../lib/server/functions/sendMessage'; import { setUserAvatar } from '../../lib/server/functions/setUserAvatar'; diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index cc4e116226665..26ea1256c3c34 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -1,4 +1,4 @@ -import { api, FederationMatrix } from '@rocket.chat/core-services'; +import { api, FederationMatrix, isMeteorError } from '@rocket.chat/core-services'; import type { IUser, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { validateFederatedUsername } from '@rocket.chat/federation-matrix'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; @@ -10,6 +10,11 @@ import { addUsersToRoomMethod, sanitizeUsername } from '../../lib/server/methods import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/server/slashCommand'; +// Type guards for the error +function isStringError(error: unknown): error is { error: string } { + return typeof (error as any)?.error === 'string'; +} + /* * Invite is a named function that will replace /invite commands * @param {Object} message - The message object @@ -96,6 +101,7 @@ slashCommands.add({ await Promise.all( usersFiltered.map(async (user) => { try { + // TODO: Refactor this to return an error if some user fails to be added return await addUsersToRoomMethod( userId, { @@ -104,23 +110,35 @@ slashCommands.add({ }, inviter, ); - } catch ({ error }: any) { - if (typeof error !== 'string') { + } catch (e: unknown) { + if (isMeteorError(e)) { + if (e.error === 'error-only-compliant-users-can-be-added-to-abac-rooms') { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t(e.error, { lng: settings.get('Language') || 'en' }), + }); + } else { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t(e.message, { lng: settings.get('Language') || 'en' }), + }); + } return; } - if (error === 'error-federated-users-in-non-federated-rooms') { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('You_cannot_add_external_users_to_non_federated_room', { lng: settings.get('Language') || 'en' }), - }); - } else if (error === 'cant-invite-for-direct-room') { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Cannot_invite_users_to_direct_rooms', { lng: settings.get('Language') || 'en' }), - }); - } else { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t(error, { lng: settings.get('Language') || 'en' }), - }); + if (isStringError(e)) { + const { error } = e; + if (error === 'error-federated-users-in-non-federated-rooms') { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('You_cannot_add_external_users_to_non_federated_room', { lng: settings.get('Language') || 'en' }), + }); + } else if (error === 'cant-invite-for-direct-room') { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('Cannot_invite_users_to_direct_rooms', { lng: settings.get('Language') || 'en' }), + }); + } else { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t(error, { lng: settings.get('Language') || 'en' }), + }); + } } } }), diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index 6497324ae9e0e..2a70552ef839f 100644 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ b/apps/meteor/app/slashcommands-join/server/server.ts @@ -1,6 +1,6 @@ import { api, Room } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions } from '@rocket.chat/models'; +import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; @@ -43,7 +43,13 @@ slashCommands.add({ }); } - await Room.join({ room, user: { _id: userId } }); + const user = await Users.findOneById(userId, { projection: { federated: 1, federation: 1 } }); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'slashCommands', + }); + } + await Room.join({ room, user }); }, options: { description: 'Join_the_given_channel', diff --git a/apps/meteor/app/slashcommands-leave/server/leave.ts b/apps/meteor/app/slashcommands-leave/server/leave.ts index fa108fe18c725..4eafeea0d0cfc 100644 --- a/apps/meteor/app/slashcommands-leave/server/leave.ts +++ b/apps/meteor/app/slashcommands-leave/server/leave.ts @@ -18,14 +18,15 @@ const Leave = async function Leave({ message, userId }: SlashCommandCallbackPara return; } await leaveRoomMethod(user, message.rid); - } catch ({ error }: any) { - if (typeof error !== 'string') { - return; + } catch (error: any) { + if (typeof error.error !== 'string') { + throw error; } const user = await Users.findOneById(userId); void api.broadcast('notify.ephemeralMessage', userId, message.rid, { msg: i18n.t(error, { lng: user?.language || settings.get('Language') || 'en' }), }); + throw new Meteor.Error(error.error, error.message); } }; diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts index 93ac1b01e6719..697f87716a903 100644 --- a/apps/meteor/app/slashcommands-topic/client/topic.ts +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -1,8 +1,8 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; +import { clientCallbacks } from '@rocket.chat/ui-client'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { Rooms } from '../../../client/stores'; -import { callbacks } from '../../../lib/callbacks'; import { hasPermission } from '../../authorization/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { slashCommands } from '../../utils/client/slashCommand'; @@ -13,7 +13,7 @@ slashCommands.add({ if (hasPermission('edit-room', message.rid)) { try { await sdk.call('saveRoomSettings', message.rid, 'roomTopic', params); - await callbacks.run('roomTopicChanged', Rooms.state.get(message.rid)); + await clientCallbacks.run('roomTopicChanged', Rooms.state.get(message.rid)); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); throw error; diff --git a/apps/meteor/app/statistics/server/functions/otrStats.ts b/apps/meteor/app/statistics/server/functions/otrStats.ts deleted file mode 100644 index 6553cae42c054..0000000000000 --- a/apps/meteor/app/statistics/server/functions/otrStats.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Rooms } from '@rocket.chat/models'; - -import { updateCounter } from './updateStatsCounter'; -import telemetryEvent from '../lib/telemetryEvents'; - -type otrDataType = { rid: string }; - -export async function otrStats(data: otrDataType) { - updateCounter({ settingsId: 'OTR_Count' }); - - await Rooms.setOTRForDMByRoomID(data.rid); -} - -telemetryEvent.register('otrStats', otrStats); diff --git a/apps/meteor/app/statistics/server/index.ts b/apps/meteor/app/statistics/server/index.ts index 6c0565688e98a..15020aea451a6 100644 --- a/apps/meteor/app/statistics/server/index.ts +++ b/apps/meteor/app/statistics/server/index.ts @@ -1,7 +1,6 @@ import './methods/getStatistics'; import './startup/monitor'; import './functions/slashCommandsStats'; -import './functions/otrStats'; export { statistics } from './lib/statistics'; export { getLastStatistics } from './functions/getLastStatistics'; diff --git a/apps/meteor/app/statistics/server/lib/getEEStatistics.ts b/apps/meteor/app/statistics/server/lib/getEEStatistics.ts index 0e04a4fab57b7..1b8b825693aa7 100644 --- a/apps/meteor/app/statistics/server/lib/getEEStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getEEStatistics.ts @@ -5,8 +5,6 @@ import type { IStats } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { CannedResponse, OmnichannelServiceLevelAgreements, LivechatRooms, LivechatTag, LivechatUnit, Users } from '@rocket.chat/models'; -import { getVoIPStatistics } from './getVoIPStatistics'; - type ENTERPRISE_STATISTICS = IStats['enterprise']; type GenericStats = Pick; @@ -92,9 +90,6 @@ async function getEEStatistics(): Promise { return true; }), ); - - // NOTE: keeping this for compatibility with current stats. Will be removed next major - statistics.omnichannelPdfTranscriptRequested = 0; // Number of PDF transcript that succeeded statsPms.push( LivechatRooms.countRoomsWithTranscriptSent().then((count) => { @@ -102,13 +97,6 @@ async function getEEStatistics(): Promise { }), ); - // TeamCollab VoIP data - statsPms.push( - getVoIPStatistics().then((voip) => { - statistics.voip = voip; - }), - ); - await Promise.all(statsPms).catch(log); return statistics as EEOnlyStats; diff --git a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts index eeb25f0d7f8fd..70a0ee73ccfe0 100644 --- a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts @@ -129,10 +129,6 @@ export async function getServicesStatistics(): Promise> enabled: settings.get('Accounts_OAuth_Nextcloud'), users: await Users.countActiveUsersByService('nextcloud', { readPreference }), }, - tokenpass: { - enabled: settings.get('Accounts_OAuth_Tokenpass'), - users: await Users.countActiveUsersByService('tokenpass', { readPreference }), - }, twitter: { enabled: settings.get('Accounts_OAuth_Twitter'), users: await Users.countActiveUsersByService('twitter', { readPreference }), diff --git a/apps/meteor/app/statistics/server/lib/getVoIPStatistics.ts b/apps/meteor/app/statistics/server/lib/getVoIPStatistics.ts deleted file mode 100644 index 99d574cdeed4c..0000000000000 --- a/apps/meteor/app/statistics/server/lib/getVoIPStatistics.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { log } from 'console'; - -import type { IStats, IVoIPPeriodStats } from '@rocket.chat/core-typings'; -import { FreeSwitchChannel } from '@rocket.chat/models'; -import { MongoInternals } from 'meteor/mongo'; - -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; - -const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; - -const getMinDate = (days?: number): Date | undefined => { - if (!days) { - return; - } - - const date = new Date(); - date.setDate(date.getDate() - days); - - return date; -}; - -async function getVoIPStatisticsForPeriod(days?: number): Promise { - const promises: Array> = []; - const options = { - readPreference: readSecondaryPreferred(db), - }; - - const minDate = getMinDate(days); - - const statistics: IVoIPPeriodStats = {}; - - promises.push( - FreeSwitchChannel.countChannelsByKind('internal', minDate, options).then((count) => { - statistics.internalCalls = count; - }), - ); - - promises.push( - FreeSwitchChannel.countChannelsByKindAndDirection('external', 'inbound', minDate, options).then((count) => { - statistics.externalInboundCalls = count; - }), - ); - - promises.push( - FreeSwitchChannel.countChannelsByKindAndDirection('external', 'outbound', minDate, options).then((count) => { - statistics.externalOutboundCalls = count; - }), - ); - - promises.push( - FreeSwitchChannel.sumChannelsDurationByKind('internal', minDate, options).then((callsDuration) => { - statistics.callsDuration = callsDuration; - }), - ); - - promises.push( - FreeSwitchChannel.countChannelsByKindAndSuccessState('internal', true, minDate, options).then((count) => { - statistics.successfulCalls = count; - }), - ); - - promises.push( - FreeSwitchChannel.countChannelsByKindAndSuccessState('internal', false, minDate, options).then((count) => { - statistics.failedCalls = count; - }), - ); - - await Promise.allSettled(promises).catch(log); - - statistics.externalCalls = (statistics.externalInboundCalls || 0) + (statistics.externalOutboundCalls || 0); - statistics.calls = (statistics.successfulCalls || 0) + (statistics.failedCalls || 0); - - return statistics; -} - -export async function getVoIPStatistics(): Promise { - const statistics: IStats['enterprise']['voip'] = {}; - - const promises = [ - getVoIPStatisticsForPeriod().then((total) => { - statistics.total = total; - }), - getVoIPStatisticsForPeriod(30).then((month) => { - statistics.lastMonth = month; - }), - getVoIPStatisticsForPeriod(7).then((week) => { - statistics.lastWeek = week; - }), - getVoIPStatisticsForPeriod(1).then((day) => { - statistics.lastDay = day; - }), - ]; - - await Promise.allSettled(promises).catch(log); - - return statistics; -} diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 0fadf701d4657..29f09fef5386f 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -4,6 +4,7 @@ import os from 'os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { NotificationQueue, Rooms, @@ -25,6 +26,7 @@ import { Subscriptions, Users, LivechatRooms, + AbacAttributes, } from '@rocket.chat/models'; import { MongoInternals } from 'meteor/mongo'; import moment from 'moment'; @@ -243,43 +245,6 @@ export const statistics = { }), ); - // VoIP Enabled - statistics.voipEnabled = settings.get('VoIP_Enabled'); - - // Amount of VoIP Calls - statsPms.push( - Rooms.countByType('v').then((count) => { - statistics.voipCalls = count; - }), - ); - - // Amount of VoIP Extensions connected - statsPms.push( - Users.countDocuments({ extension: { $exists: true } }).then((count) => { - statistics.voipExtensions = count; - }), - ); - - // Amount of Calls that ended properly - statsPms.push( - Messages.countByType('voip-call-wrapup', { readPreference }).then((count) => { - statistics.voipSuccessfulCalls = count; - }), - ); - - // Amount of Calls that ended with an error - statsPms.push( - Messages.countByType('voip-call-ended-unexpectedly', { readPreference }).then((count) => { - statistics.voipErrorCalls = count; - }), - ); - // Amount of Calls that were put on hold - statsPms.push( - Messages.countRoomsWithMessageType('voip-call-on-hold', { readPreference }).then((count) => { - statistics.voipOnHoldCalls = count; - }), - ); - const defaultValue = { contactsCount: 0, conversationsCount: 0, sources: [] }; const billablePeriod = moment.utc().format('YYYY-MM'); statsPms.push( @@ -415,9 +380,9 @@ export const statistics = { }), ); - const { oplogEnabled, mongoVersion, mongoStorageEngine } = await getMongoInfo(); + const { mongoVersion, mongoStorageEngine } = await getMongoInfo(); statistics.msEnabled = isRunningMs(); - statistics.oplogEnabled = oplogEnabled; + statistics.oplogEnabled = false; statistics.mongoVersion = mongoVersion; statistics.mongoStorageEngine = mongoStorageEngine || ''; @@ -540,8 +505,6 @@ export const statistics = { statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count'); statistics.joinJitsiButton = settings.get('Jitsi_Click_To_Join_Count'); statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); - statistics.totalOTRRooms = await Rooms.countByCreatedOTR({ readPreference }); - statistics.totalOTR = settings.get('OTR_Count'); statistics.totalBroadcastRooms = await Rooms.countByBroadcast({ readPreference }); statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference }); @@ -604,10 +567,25 @@ export const statistics = { statistics.matrixFederation = await getMatrixFederationStatistics(); - // Omnichannel call stats - statistics.webRTCEnabled = settings.get('WebRTC_Enabled'); - statistics.webRTCEnabledForOmnichannel = settings.get('Omnichannel_call_provider') === 'WebRTC'; - statistics.omnichannelWebRTCCalls = await Rooms.findCountOfRoomsWithActiveCalls(); + // ABAC stats + if (License.hasModule('abac')) { + statistics.abacEnabled = settings.get('ABAC_Enabled'); + statsPms.push( + AbacAttributes.estimatedDocumentCount().then((result) => { + statistics.abacTotalAttributes = result; + }), + ); + statsPms.push( + AbacAttributes.countTotalValues().then((result) => { + statistics.abacTotalAttributeValues = result; + }), + ); + statsPms.push( + Rooms.countAbacEnabled().then((result) => { + statistics.abacRoomsEnrolled = result; + }), + ); + } await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts index a938dadddb276..5a3eea24f47b3 100644 --- a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts +++ b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts @@ -3,7 +3,7 @@ import { isEditedMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; import { updateThreadUsersSubscriptions, getMentions } from '../../../lib/server/lib/notifyUsersOnMessage'; import { sendMessageNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; diff --git a/apps/meteor/app/threads/server/methods/getThreadMessages.ts b/apps/meteor/app/threads/server/methods/getThreadMessages.ts index 8ae31130df1b0..ab5e8a5e3f00d 100644 --- a/apps/meteor/app/threads/server/methods/getThreadMessages.ts +++ b/apps/meteor/app/threads/server/methods/getThreadMessages.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; +import { callbacks } from '../../../../server/lib/callbacks'; import { canAccessRoomAsync } from '../../../authorization/server'; import { settings } from '../../../settings/server'; import { readThread } from '../functions'; @@ -55,7 +55,8 @@ Meteor.methods({ ...(limit && { limit }), sort: { ts: -1 }, }).toArray(); - callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); + + callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid }); return [thread, ...result]; }, diff --git a/apps/meteor/app/tokenpass/server/index.ts b/apps/meteor/app/tokenpass/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/tokenpass/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/tokenpass/server/lib.ts b/apps/meteor/app/tokenpass/server/lib.ts deleted file mode 100644 index 32087ff604375..0000000000000 --- a/apps/meteor/app/tokenpass/server/lib.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -const config: OauthConfig = { - serverURL: '', - identityPath: '/oauth/user', - authorizePath: '/oauth/authorize', - tokenPath: '/oauth/access-token', - scope: 'user', - tokenSentVia: 'payload', - usernameField: 'username', - mergeUsers: true, - addAutopublishFields: { - forLoggedInUser: ['services.tokenpass'], - forOtherUsers: ['services.tokenpass.name'], - }, - accessTokenParam: 'access_token', -}; - -const Tokenpass = new CustomOAuth('tokenpass', config); - -Meteor.startup(() => { - settings.watch('API_Tokenpass_URL', (value) => { - config.serverURL = value; - Tokenpass.configure(config); - }); -}); diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index 85ebaa8ccba6e..b7e423b51d655 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -16,8 +16,6 @@ window.addEventListener('load', function() { ${process.env.DISABLE_ANIMATION ? 'window.DISABLE_ANIMATION = true;\n' : ''} -${settings.get('API_Use_REST_For_DDP_Calls') ? 'window.USE_REST_FOR_DDP_CALLS = true;\n' : ''} -${settings.get('ECDH_Enabled') ? 'window.ECDH_Enabled = true;\n' : ''} // Custom_Script_Logged_Out window.addEventListener('Custom_Script_Logged_Out', function() { ${settings.get('Custom_Script_Logged_Out')} @@ -53,14 +51,7 @@ window.addEventListener('load', function() { }`; settings.watchMultiple( - [ - 'API_Use_REST_For_DDP_Calls', - 'Custom_Script_Logged_Out', - 'Custom_Script_Logged_In', - 'Custom_Script_On_Logout', - 'Accounts_ForgetUserSessionOnWindowClose', - 'ECDH_Enabled', - ], + ['Custom_Script_Logged_Out', 'Custom_Script_Logged_In', 'Custom_Script_On_Logout', 'Accounts_ForgetUserSessionOnWindowClose'], () => { const content = getContent(); addScript('scripts', content); diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index c24a770c68fac..d6a8abf9c35da 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -1,6 +1,7 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; +import { clientCallbacks } from '@rocket.chat/ui-client'; import type { Filter } from 'mongodb'; import { upsertMessage, RoomHistoryManager } from './RoomHistoryManager'; @@ -10,7 +11,6 @@ import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { modifyMessageOnFilesDelete } from '../../../../client/lib/utils/modifyMessageOnFilesDelete'; import { Messages, Subscriptions } from '../../../../client/stores'; -import { callbacks } from '../../../../lib/callbacks'; import { sdk } from '../../../utils/client/lib/SDKClient'; const maxRoomsOpen = parseInt(getConfig('maxRoomsOpen') ?? '5') || 5; @@ -180,11 +180,11 @@ const openRoom = (typeName: string, record: OpenedRoom) => { ({ _id: msg._id, temp: { $ne: true } }); await upsertMessage({ msg, subscription }); if (isNew) { - await callbacks.run('streamNewMessage', msg); + await clientCallbacks.run('streamNewMessage', msg); } } - await callbacks.run('streamMessage', { ...msg, name: room.name || '' }); + await clientCallbacks.run('streamMessage', { ...msg, name: room.name || '' }); fireGlobalEvent('new-message', { ...msg, diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index 219c297406bc1..3ea1d39f4f2f6 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -6,7 +6,7 @@ import { invokeTwoFactorModal } from '../../../../client/lib/2fa/process2faRetur import { baseURI } from '../../../../client/lib/baseURI'; class RestApiClient extends RestClient { - getCredentials(): + override getCredentials(): | { 'X-User-Id': string; 'X-Auth-Token': string; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 43d018b95970d..c8fc9d884669c 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.13.2" + "version": "8.0.0-rc.5" } diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index a366f95a2fe54..6ee9992826727 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -29,7 +29,7 @@ export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'oauth.authorizedClients': 1, '_updatedAt': 1, 'avatarETag': 1, - 'extension': 1, 'openBusinessHours': 1, + 'abacAttributes': 1, ...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }), }); diff --git a/apps/meteor/app/utils/server/functions/getMongoInfo.ts b/apps/meteor/app/utils/server/functions/getMongoInfo.ts index 1caef4a22e329..82482e361832b 100644 --- a/apps/meteor/app/utils/server/functions/getMongoInfo.ts +++ b/apps/meteor/app/utils/server/functions/getMongoInfo.ts @@ -1,17 +1,12 @@ import { MongoInternals } from 'meteor/mongo'; -import { isWatcherRunning } from '../../../../server/modules/watchers/watchers.module'; - -function getOplogInfo(): { oplogEnabled: boolean; mongo: MongoConnection } { +function getOplogInfo(): { mongo: MongoConnection } { const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); - const oplogEnabled = isWatcherRunning(); - - return { oplogEnabled, mongo }; + return { mongo }; } async function fallbackMongoInfo(): Promise<{ - oplogEnabled: boolean; mongoVersion: string; mongoStorageEngine?: string; mongo: MongoConnection; @@ -19,7 +14,7 @@ async function fallbackMongoInfo(): Promise<{ let mongoVersion; let mongoStorageEngine; - const { oplogEnabled, mongo } = getOplogInfo(); + const { mongo } = getOplogInfo(); try { const { version } = await mongo.db.command({ buildinfo: 1 }); @@ -40,11 +35,10 @@ async function fallbackMongoInfo(): Promise<{ console.error('=================================='); } - return { oplogEnabled, mongoVersion, mongoStorageEngine, mongo }; + return { mongoVersion, mongoStorageEngine, mongo }; } export async function getMongoInfo(): Promise<{ - oplogEnabled: boolean; mongoVersion: string; mongoStorageEngine?: string; mongo: MongoConnection; @@ -52,7 +46,7 @@ export async function getMongoInfo(): Promise<{ let mongoVersion; let mongoStorageEngine; - const { oplogEnabled, mongo } = getOplogInfo(); + const { mongo } = getOplogInfo(); try { const { version, storageEngine } = await mongo.db.command({ serverStatus: 1 }); @@ -63,5 +57,5 @@ export async function getMongoInfo(): Promise<{ return fallbackMongoInfo(); } - return { oplogEnabled, mongoVersion, mongoStorageEngine, mongo }; + return { mongoVersion, mongoStorageEngine, mongo }; } diff --git a/apps/meteor/app/voip/server/startup.ts b/apps/meteor/app/voip/server/startup.ts deleted file mode 100644 index 1c27b913d103c..0000000000000 --- a/apps/meteor/app/voip/server/startup.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { VoipAsterisk } from '@rocket.chat/core-services'; - -import { settings } from '../../settings/server'; - -settings.watch('VoIP_Enabled', async (value: boolean) => { - try { - if (value) { - await VoipAsterisk.init(); - } else { - await VoipAsterisk.stop(); - } - } catch (e) { - // do nothing - } -}); - -settings.changeMultiple( - ['VoIP_Management_Server_Host', 'VoIP_Management_Server_Port', 'VoIP_Management_Server_Username', 'VoIP_Management_Server_Password'], - async (_values) => { - // Here, if 4 settings are changed at once, we're getting 4 diff callbacks. The good part is that all callbacks are fired almost instantly - // So to avoid stopping/starting voip too often, we debounce the call and restart 1 second after the last setting has reached us. - if (settings.get('VoIP_Enabled')) { - try { - await VoipAsterisk.refresh(); - } catch (e) { - // do nothing - } - } - }, -); diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.ts b/apps/meteor/app/webrtc/client/WebRTCClass.ts deleted file mode 100644 index 28877346bacd6..0000000000000 --- a/apps/meteor/app/webrtc/client/WebRTCClass.ts +++ /dev/null @@ -1,1066 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat/ddp-client'; -import { Emitter } from '@rocket.chat/emitter'; -import { GenericModal, imperativeModal } from '@rocket.chat/ui-client'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { ChromeScreenShare } from './screenShare'; -import { settings } from '../../../client/lib/settings'; -import { getUserId } from '../../../client/lib/user'; -import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; -import { Subscriptions, Users } from '../../../client/stores'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; -import { WEB_RTC_EVENTS } from '../lib/constants'; - -// FIXME: there is a mix of obsolete definitions and incorrect field assignments - -declare global { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface RTCPeerConnection { - /** @deprecated non-standard */ - createdAt: number; - /** @deprecated non-standard */ - remoteMedia: MediaStreamConstraints; - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface RTCOfferOptions { - /** @deprecated non-standard */ - mandatory?: unknown; - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface MediaStream { - /** @deprecated non-standard */ - volume?: GainNode; - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface MediaStreamConstraints { - /** @deprecated non-standard */ - desktop?: boolean; - } - - /** @deprecated browser-specific global */ - const chrome: { - webstore: { - install(url: string, onSuccess: () => void, onError: (error: any) => void): void; - }; - }; - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Window { - rocketchatscreenshare?: unknown; - audioContext?: AudioContext; - } -} - -type EventData, TType> = Extract< - StreamerCallbackArgs, - [type: TType, data: any] ->[1]; - -type StatusData = EventData<'notify-room', `${string}/webrtc`, 'status'>; -type CallData = EventData<'notify-room-users', `${string}/webrtc`, 'call'>; -export type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; -export type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; -export type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; - -type RemoteItem = { - id: string; - url: MediaStream; - state: RTCIceConnectionState; - stateText?: string; - connected?: boolean; -}; - -type RemoteConnection = { - id: string; - media: MediaStreamConstraints; -}; - -class WebRTCTransportClass extends Emitter<{ - status: StatusData; - call: CallData; - candidate: CandidateData; - description: DescriptionData; - join: JoinData; -}> { - public debug = false; - - constructor(public webrtcInstance: WebRTCClass) { - super(); - sdk.stream('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { - this.log('WebRTCTransportClass - onRoom', type, data); - this.emit(type, data); - }); - } - - log(...args: unknown[]) { - if (this.debug === true) { - console.log(...args); - } - } - - onUserStream(type: 'candidate', data: CandidateData): void; - - onUserStream(type: 'description', data: DescriptionData): void; - - onUserStream(type: 'join', data: JoinData): void; - - onUserStream( - ...[type, data]: - | [type: 'candidate', data: CandidateData] - | [type: 'description', data: DescriptionData] - | [type: 'join', data: JoinData] - ) { - if (data.room !== this.webrtcInstance.room) { - return; - } - - this.log('WebRTCTransportClass - onUser', type, data); - - switch (type) { - case 'candidate': - this.emit('candidate', data); - break; - case 'description': - this.emit('description', data); - break; - case 'join': - this.emit('join', data); - break; - } - } - - startCall(data: CallData) { - this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId); - sdk.publish('notify-room-users', [ - `${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, - WEB_RTC_EVENTS.CALL, - { - from: this.webrtcInstance.selfId, - room: this.webrtcInstance.room, - media: data.media, - monitor: data.monitor, - }, - ]); - } - - joinCall(data: JoinData) { - this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId); - if (data.monitor === true) { - sdk.publish('notify-user', [ - `${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, - WEB_RTC_EVENTS.JOIN, - { - from: this.webrtcInstance.selfId, - room: this.webrtcInstance.room, - media: data.media, - monitor: data.monitor, - }, - ]); - } else { - sdk.publish('notify-room-users', [ - `${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, - WEB_RTC_EVENTS.JOIN, - { - from: this.webrtcInstance.selfId, - room: this.webrtcInstance.room, - media: data.media, - monitor: data.monitor, - }, - ]); - } - } - - sendCandidate(data: CandidateData) { - data.from = this.webrtcInstance.selfId; - data.room = this.webrtcInstance.room; - this.log('WebRTCTransportClass - sendCandidate', data); - sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.CANDIDATE, data]); - } - - sendDescription(data: DescriptionData) { - data.from = this.webrtcInstance.selfId; - data.room = this.webrtcInstance.room; - this.log('WebRTCTransportClass - sendDescription', data); - sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.DESCRIPTION, data]); - } - - sendStatus(data: StatusData) { - this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); - data.from = this.webrtcInstance.selfId; - sdk.publish('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.STATUS, data]); - } - - onRemoteCall(fn: (data: CallData) => void) { - return this.on(WEB_RTC_EVENTS.CALL, fn); - } - - onRemoteJoin(fn: (data: JoinData) => void) { - return this.on(WEB_RTC_EVENTS.JOIN, fn); - } - - onRemoteCandidate(fn: (data: CandidateData) => void) { - return this.on(WEB_RTC_EVENTS.CANDIDATE, fn); - } - - onRemoteDescription(fn: (data: DescriptionData) => void) { - return this.on(WEB_RTC_EVENTS.DESCRIPTION, fn); - } - - onRemoteStatus(fn: (data: StatusData) => void) { - return this.on(WEB_RTC_EVENTS.STATUS, fn); - } -} - -class WebRTCClass { - transport: WebRTCTransportClass; - - config: { iceServers: RTCIceServer[] }; - - debug: boolean; - - TransportClass: typeof WebRTCTransportClass; - - peerConnections: Record = {}; - - remoteItems: ReactiveVar; - - remoteItemsById: ReactiveVar>; - - callInProgress: ReactiveVar; - - audioEnabled: ReactiveVar; - - videoEnabled: ReactiveVar; - - overlayEnabled: ReactiveVar; - - screenShareEnabled: ReactiveVar; - - localUrl: ReactiveVar; - - active: boolean; - - remoteMonitoring: boolean; - - monitor: boolean; - - navigator: string | undefined; - - screenShareAvailable: boolean; - - media: MediaStreamConstraints; - - constructor( - public selfId: string, - public room: string, - public autoAccept = false, - ) { - this.config = { - iceServers: [], - }; - this.debug = false; - this.TransportClass = WebRTCTransportClass; - let servers = settings.peek('WebRTC_Servers'); - if (servers && servers.trim() !== '') { - servers = servers.replace(/\s/g, ''); - - servers.split(',').forEach((server) => { - const parts = server.split('@'); - const serverConfig: RTCIceServer = { - urls: parts.pop()!, - }; - if (parts.length === 1) { - const [username, credential] = parts[0].split(':'); - serverConfig.username = decodeURIComponent(username); - serverConfig.credential = decodeURIComponent(credential); - } - this.config.iceServers.push(serverConfig); - }); - } - this.peerConnections = {}; - this.remoteItems = new ReactiveVar([]); - this.remoteItemsById = new ReactiveVar({}); - this.callInProgress = new ReactiveVar(false); - this.audioEnabled = new ReactiveVar(false); - this.videoEnabled = new ReactiveVar(false); - this.overlayEnabled = new ReactiveVar(false); - this.screenShareEnabled = new ReactiveVar(false); - this.localUrl = new ReactiveVar(undefined); - this.active = false; - this.remoteMonitoring = false; - this.monitor = false; - this.navigator = undefined; - const userAgent = navigator.userAgent.toLocaleLowerCase(); - - if (userAgent.indexOf('electron') !== -1) { - this.navigator = 'electron'; - } else if (userAgent.indexOf('chrome') !== -1) { - this.navigator = 'chrome'; - } else if (userAgent.indexOf('firefox') !== -1) { - this.navigator = 'firefox'; - } else if (userAgent.indexOf('safari') !== -1) { - this.navigator = 'safari'; - } - - this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator!); - this.media = { - video: true, - audio: true, - }; - this.transport = new this.TransportClass(this); - this.transport.onRemoteCall(this.onRemoteCall.bind(this)); - this.transport.onRemoteJoin(this.onRemoteJoin.bind(this)); - this.transport.onRemoteCandidate(this.onRemoteCandidate.bind(this)); - this.transport.onRemoteDescription(this.onRemoteDescription.bind(this)); - this.transport.onRemoteStatus(this.onRemoteStatus.bind(this)); - - setInterval(this.checkPeerConnections.bind(this), 1000); - } - - onUserStream(type: 'candidate', data: CandidateData): void; - - onUserStream(type: 'description', data: DescriptionData): void; - - onUserStream(type: 'join', data: JoinData): void; - - onUserStream( - ...[type, data]: - | [type: 'candidate', data: CandidateData] - | [type: 'description', data: DescriptionData] - | [type: 'join', data: JoinData] - ) { - switch (type) { - case 'candidate': - this.transport.onUserStream('candidate', data); - break; - - case 'description': - this.transport.onUserStream('description', data); - break; - - case 'join': - this.transport.onUserStream('join', data); - break; - } - } - - log(...args: unknown[]) { - if (this.debug === true) { - console.log(...args); - } - } - - onError(...args: unknown[]) { - console.error(...args); - } - - checkPeerConnections() { - const { peerConnections } = this; - const date = Date.now(); - Object.entries(peerConnections).some(([id, peerConnection]) => { - if (!['connected', 'completed'].includes(peerConnection.iceConnectionState) && peerConnection.createdAt + 5000 < date) { - this.stopPeerConnection(id); - return true; - } - return false; - }); - } - - updateRemoteItems() { - const items: RemoteItem[] = []; - const itemsById: Record = {}; - const { peerConnections } = this; - - Object.entries(peerConnections).forEach(([id, peerConnection]) => { - peerConnection.getRemoteStreams().forEach((remoteStream) => { - const item: RemoteItem = { - id, - url: remoteStream, - state: peerConnection.iceConnectionState, - }; - switch (peerConnection.iceConnectionState) { - case 'checking': - item.stateText = 'Connecting...'; - break; - case 'connected': - case 'completed': - item.stateText = 'Connected'; - item.connected = true; - break; - case 'disconnected': - item.stateText = 'Disconnected'; - break; - case 'failed': - item.stateText = 'Failed'; - break; - case 'closed': - item.stateText = 'Closed'; - } - items.push(item); - itemsById[id] = item; - }); - }); - this.remoteItems.set(items); - this.remoteItemsById.set(itemsById); - } - - resetCallInProgress = () => { - this.callInProgress.set(false); - }; - - broadcastStatus() { - if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) { - return; - } - const remoteConnections: RemoteConnection[] = []; - const { peerConnections } = this; - Object.entries(peerConnections).forEach(([id, { remoteMedia: media }]) => { - remoteConnections.push({ - id, - media, - }); - }); - - this.transport.sendStatus({ - media: this.media, - remoteConnections, - }); - } - - callInProgressTimeout: ReturnType | undefined = undefined; - - onRemoteStatus(data: StatusData) { - // this.log(onRemoteStatus, arguments); - this.callInProgress.set(true); - clearTimeout(this.callInProgressTimeout); - this.callInProgressTimeout = setTimeout(this.resetCallInProgress, 2000); - if (this.active !== true) { - return; - } - const remoteConnections = [ - { - id: data.from!, - media: data.media, - }, - ...data.remoteConnections, - ]; - - remoteConnections.forEach((remoteConnection) => { - if (remoteConnection.id !== this.selfId && this.peerConnections[remoteConnection.id] == null) { - this.log('reconnecting with', remoteConnection.id); - this.onRemoteJoin({ - from: remoteConnection.id, - media: remoteConnection.media, - }); - } - }); - } - - getPeerConnection(id: string) { - if (this.peerConnections[id] != null) { - return this.peerConnections[id]; - } - const peerConnection = new RTCPeerConnection(this.config); - - peerConnection.createdAt = Date.now(); - peerConnection.remoteMedia = {}; - this.peerConnections[id] = peerConnection; - const eventNames = [ - 'icecandidate', - 'addstream', - 'removestream', - 'iceconnectionstatechange', - 'datachannel', - 'identityresult', - 'idpassertionerror', - 'idpvalidationerror', - 'negotiationneeded', - 'peeridentity', - 'signalingstatechange', - ]; - - eventNames.forEach((eventName) => { - peerConnection.addEventListener(eventName, (e) => { - this.log(id, e.type, e); - }); - }); - - peerConnection.addEventListener('icecandidate', (e) => { - if (e.candidate == null) { - return; - } - this.transport.sendCandidate({ - to: id, - candidate: { - candidate: e.candidate.candidate, - sdpMLineIndex: e.candidate.sdpMLineIndex, - sdpMid: e.candidate.sdpMid, - }, - }); - }); - peerConnection.addEventListener('addstream', () => { - this.updateRemoteItems(); - }); - peerConnection.addEventListener('removestream', () => { - this.updateRemoteItems(); - }); - peerConnection.addEventListener('iceconnectionstatechange', () => { - if ( - (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'closed') && - peerConnection === this.peerConnections[id] - ) { - this.stopPeerConnection(id); - setTimeout(() => { - if (Object.keys(this.peerConnections).length === 0) { - this.stop(); - } - }, 3000); - } - this.updateRemoteItems(); - }); - return peerConnection; - } - - audioContext: AudioContext | undefined; - - _getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error?: any) => void) { - const onSuccessLocal = (stream: MediaStream) => { - if (AudioContext && stream.getAudioTracks().length > 0) { - const audioContext = new AudioContext(); - const source = audioContext.createMediaStreamSource(stream); - const volume = audioContext.createGain(); - source.connect(volume); - const peer = audioContext.createMediaStreamDestination(); - volume.connect(peer); - volume.gain.value = 0.6; - stream.removeTrack(stream.getAudioTracks()[0]); - stream.addTrack(peer.stream.getAudioTracks()[0]); - stream.volume = volume; - this.audioContext = audioContext; - } - onSuccess(stream); - }; - - if (navigator.mediaDevices?.getUserMedia) { - return navigator.mediaDevices.getUserMedia(media).then(onSuccessLocal).catch(onError); - } - - navigator.getUserMedia?.(media, onSuccessLocal, onError); - } - - getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error: any) => void = this.onError) { - if (media.desktop !== true) { - void this._getUserMedia(media, onSuccess, onError); - return; - } - if (this.screenShareAvailable !== true) { - console.log('Screen share is not avaliable'); - return; - } - const getScreen = (audioStream?: MediaStream) => { - const refresh = function () { - imperativeModal.open({ - component: GenericModal, - props: { - variant: 'warning', - title: t('Refresh_your_page_after_install_to_enable_screen_sharing'), - }, - }); - }; - - const isChromeExtensionInstalled = this.navigator === 'chrome' && ChromeScreenShare.installed; - const isFirefoxExtensionInstalled = this.navigator === 'firefox' && window.rocketchatscreenshare != null; - - if (!isChromeExtensionInstalled && !isFirefoxExtensionInstalled) { - imperativeModal.open({ - component: GenericModal, - props: { - title: t('Screen_Share'), - variant: 'warning', - confirmText: t('Install_Extension'), - cancelText: t('Cancel'), - children: t('You_need_install_an_extension_to_allow_screen_sharing'), - onConfirm: () => { - if (this.navigator === 'chrome') { - const url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf'; - try { - chrome.webstore.install(url, refresh, () => { - window.open(url); - refresh(); - }); - } catch (_error) { - console.log(_error); - window.open(url); - refresh(); - } - } else if (this.navigator === 'firefox') { - window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/'); - refresh(); - } - }, - }, - }); - - return onError(false); - } - - const getScreenSuccess = (stream: MediaStream) => { - if (audioStream != null) { - stream.addTrack(audioStream.getAudioTracks()[0]); - } - onSuccess(stream); - }; - if (this.navigator === 'firefox') { - media = { - audio: media.audio, - video: { - mozMediaSource: 'window', - mediaSource: 'window', - }, - }; - void this._getUserMedia(media, getScreenSuccess, onError); - } else { - ChromeScreenShare.getSourceId(this.navigator!, (id) => { - media = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: id, - maxWidth: 1280, - maxHeight: 720, - }, - }, - }; - void this._getUserMedia(media, getScreenSuccess, onError); - }); - } - }; - if (this.navigator === 'firefox' || media.audio == null || media.audio === false) { - getScreen(); - } else { - const getAudioSuccess = (audioStream: MediaStream) => { - getScreen(audioStream); - }; - const getAudioError = () => { - getScreen(); - }; - - void this._getUserMedia( - { - audio: media.audio, - }, - getAudioSuccess, - getAudioError, - ); - } - } - - getLocalUserMedia(callback: (...args: any[]) => void, ...args: unknown[]) { - this.log('getLocalUserMedia', [callback, ...args]); - if (this.localStream != null) { - return callback(null, this.localStream); - } - const onSuccess = (stream: MediaStream) => { - this.localStream = stream; - !this.audioEnabled.get() && this.disableAudio(); - !this.videoEnabled.get() && this.disableVideo(); - this.localUrl.set(stream); - const { peerConnections } = this; - Object.entries(peerConnections).forEach(([, peerConnection]) => peerConnection.addStream(stream)); - document.querySelector('video#localVideo')!.srcObject = stream; - callback(null, this.localStream); - }; - const onError = (error: any) => { - callback(false); - this.onError(error); - }; - this.getUserMedia(this.media, onSuccess, onError); - } - - stopPeerConnection = (id: string) => { - const peerConnection = this.peerConnections[id]; - if (peerConnection == null) { - return; - } - delete this.peerConnections[id]; - peerConnection.close(); - this.updateRemoteItems(); - }; - - stopAllPeerConnections() { - const { peerConnections } = this; - - Object.keys(peerConnections).forEach(this.stopPeerConnection); - - void window.audioContext?.close(); // FIXME: probably should be `this.audioContext` - } - - setAudioEnabled(enabled = true) { - if (this.localStream != null) { - this.localStream.getAudioTracks().forEach((audio) => { - audio.enabled = enabled; - }); - this.audioEnabled.set(enabled); - } - } - - disableAudio() { - this.setAudioEnabled(false); - } - - enableAudio() { - this.setAudioEnabled(true); - } - - toggleAudio() { - if (this.audioEnabled.get()) { - return this.disableAudio(); - } - return this.enableAudio(); - } - - localStream: MediaStream | undefined; - - setVideoEnabled(enabled = true) { - if (this.localStream != null) { - this.localStream.getVideoTracks().forEach((video) => { - video.enabled = enabled; - }); - this.videoEnabled.set(enabled); - } - } - - disableScreenShare() { - this.setScreenShareEnabled(false); - } - - enableScreenShare() { - this.setScreenShareEnabled(true); - } - - setScreenShareEnabled(enabled = true) { - if (this.localStream != null) { - this.media.desktop = enabled; - delete this.localStream; - this.getLocalUserMedia((err) => { - if (err != null) { - return; - } - this.screenShareEnabled.set(enabled); - this.stopAllPeerConnections(); - this.joinCall(); - }); - } - } - - disableVideo() { - this.setVideoEnabled(false); - } - - enableVideo() { - this.setVideoEnabled(true); - } - - toggleVideo() { - if (this.videoEnabled.get()) { - return this.disableVideo(); - } - return this.enableVideo(); - } - - stop() { - this.active = false; - this.monitor = false; - this.remoteMonitoring = false; - if (this.localStream != null && typeof this.localStream !== 'undefined') { - this.localStream.getTracks().forEach((track) => track.stop()); - } - this.localUrl.set(undefined); - delete this.localStream; - this.stopAllPeerConnections(); - } - - startCall(media: MediaStreamConstraints = {}, ...args: unknown[]) { - this.log('startCall', [media, ...args]); - this.media = media; - this.getLocalUserMedia(() => { - this.active = true; - this.transport.startCall({ - media: this.media, - }); - }); - } - - startCallAsMonitor(media: MediaStreamConstraints = {}, ...args: unknown[]) { - this.log('startCallAsMonitor', [media, ...args]); - this.media = media; - this.active = true; - this.monitor = true; - this.transport.startCall({ - media: this.media, - monitor: true, - }); - } - - onRemoteCall(data: CallData) { - if (this.autoAccept === true) { - setTimeout(() => { - this.joinCall({ - to: data.from, - monitor: data.monitor, - media: data.media, - }); - }, 0); - return; - } - - const username = data.from ? Users.state.get(data.from)?.username : undefined; - const subscription = Subscriptions.state.find(({ rid }) => rid === data.room); - - let icon; - let title; - if (data.monitor === true) { - icon = 'eye' as const; - title = t('WebRTC_monitor_call_from_%s', username); - } else if (subscription && subscription.t === 'd') { - if (data.media?.video) { - icon = 'video' as const; - title = t('WebRTC_direct_video_call_from_%s', username); - } else { - icon = 'phone' as const; - title = t('WebRTC_direct_audio_call_from_%s', username); - } - } else if (data.media?.video) { - icon = 'video' as const; - title = t('WebRTC_group_video_call_from_%s', subscription?.name); - } else { - icon = 'phone' as const; - title = t('WebRTC_group_audio_call_from_%s', subscription?.name); - } - - imperativeModal.open({ - component: GenericModal, - props: { - title, - icon, - confirmText: t('Yes'), - cancelText: t('No'), - children: t('Do_you_want_to_accept'), - onConfirm: () => { - void goToRoomById(data.room!); - return this.joinCall({ - to: data.from, - monitor: data.monitor, - media: data.media, - }); - }, - onCancel: () => this.stop(), - onClose: () => this.stop(), - }, - }); - } - - joinCall(data: JoinData = {}, ...args: unknown[]) { - data.media = this.media; - this.log('joinCall', [data, ...args]); - this.getLocalUserMedia(() => { - this.remoteMonitoring = data.monitor!; - this.active = true; - this.transport.joinCall(data); - }); - } - - onRemoteJoin(data: JoinData, ...args: unknown[]) { - if (this.active !== true) { - return; - } - this.log('onRemoteJoin', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from!); - - // needsRefresh = false - // if peerConnection.iceConnectionState isnt 'new' - // needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true - // needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true - // needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop - - // # if peerConnection.signalingState is "have-local-offer" or needsRefresh - - if ((peerConnection.signalingState as RTCSignalingState | 'checking') !== 'checking') { - this.stopPeerConnection(data.from!); - peerConnection = this.getPeerConnection(data.from!); - } - if (peerConnection.iceConnectionState !== 'new') { - return; - } - peerConnection.remoteMedia = data.media!; - if (this.localStream) { - peerConnection.addStream(this.localStream); - } - const onOffer: RTCSessionDescriptionCallback = (offer) => { - const onLocalDescription = () => { - this.transport.sendDescription({ - to: data.from, - type: 'offer', - ts: peerConnection.createdAt, - media: this.media, - description: { - sdp: offer.sdp, - type: offer.type, - }, - }); - }; - - void peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); - }; - - if (data.monitor === true) { - void peerConnection.createOffer(onOffer, this.onError, { - mandatory: { - OfferToReceiveAudio: data.media?.audio, - OfferToReceiveVideo: data.media?.video, - }, - }); - } else { - void peerConnection.createOffer(onOffer, this.onError); - } - } - - onRemoteOffer(data: Omit, ...args: unknown[]) { - if (this.active !== true) { - return; - } - - this.log('onRemoteOffer', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from!); - - if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && peerConnection.createdAt < data.ts) { - this.stopPeerConnection(data.from!); - peerConnection = this.getPeerConnection(data.from!); - } - - if (peerConnection.iceConnectionState !== 'new') { - return; - } - - void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); - - try { - if (this.localStream) { - peerConnection.addStream(this.localStream); - } - } catch (error) { - console.log(error); - } - - const onAnswer: RTCSessionDescriptionCallback = (answer) => { - const onLocalDescription = () => { - this.transport.sendDescription({ - to: data.from, - type: 'answer', - ts: peerConnection.createdAt, - description: { - sdp: answer.sdp, - type: answer.type, - }, - }); - }; - - void peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); - }; - - void peerConnection.createAnswer(onAnswer, this.onError); - } - - onRemoteCandidate(data: CandidateData, ...args: unknown[]) { - if (this.active !== true) { - return; - } - if (data.to !== this.selfId) { - return; - } - this.log('onRemoteCandidate', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from!); - if ( - peerConnection.iceConnectionState !== 'closed' && - peerConnection.iceConnectionState !== 'failed' && - peerConnection.iceConnectionState !== 'disconnected' && - peerConnection.iceConnectionState !== 'completed' - ) { - void peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); - } - document.querySelector('video#remoteVideo')!.srcObject = this.remoteItems.get()[0]?.url; - } - - onRemoteDescription(data: DescriptionData, ...args: unknown[]) { - if (this.active !== true) { - return; - } - if (data.to !== this.selfId) { - return; - } - this.log('onRemoteDescription', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from!); - if (data.type === 'offer') { - peerConnection.remoteMedia = data.media; - this.onRemoteOffer({ - from: data.from, - ts: data.ts, - description: data.description, - }); - } else { - void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); - } - } -} - -const WebRTC = new (class { - instancesByRoomId: Record = {}; - - constructor() { - this.instancesByRoomId = {}; - } - - getInstanceByRoomId(rid: IRoom['_id'], visitorId: string | null = null) { - let enabled = false; - if (!visitorId) { - const subscription = Subscriptions.state.find((record) => record.rid === rid); - if (!subscription) { - return; - } - switch (subscription.t) { - case 'd': - enabled = settings.peek('WebRTC_Enable_Direct') ?? false; - break; - case 'p': - enabled = settings.peek('WebRTC_Enable_Private') ?? false; - break; - case 'c': - enabled = settings.peek('WebRTC_Enable_Channel') ?? false; - break; - case 'l': - enabled = settings.peek('Omnichannel_call_provider') === 'WebRTC'; - } - } else { - enabled = settings.peek('Omnichannel_call_provider') === 'WebRTC'; - } - enabled = enabled && (settings.peek('WebRTC_Enabled') ?? false); - if (enabled === false) { - return; - } - if (this.instancesByRoomId[rid] == null) { - const uid = visitorId ?? getUserId(); - if (!uid) return undefined; - const autoAccept = !!visitorId; - this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept); - } - return this.instancesByRoomId[rid]; - } -})(); - -export { WebRTC }; diff --git a/apps/meteor/app/webrtc/client/actionLink.tsx b/apps/meteor/app/webrtc/client/actionLink.tsx deleted file mode 100644 index e3bc39743e384..0000000000000 --- a/apps/meteor/app/webrtc/client/actionLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; - -import { actionLinks } from '../../../client/lib/actionLinks'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { Rooms } from '../../../client/stores'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; - -actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => { - const room = Rooms.state.get(message.rid); - if (!room) { - throw new Error('Room not found'); - } - const { callStatus, _id } = room; - if (callStatus === 'declined' || callStatus === 'ended') { - dispatchToastMessage({ type: 'info', message: t('Call_Already_Ended') }); - return; - } - window.open(`/meet/${_id}`, _id); -}); - -actionLinks.register('endLivechatWebRTCCall', async (message: IMessage) => { - const room = Rooms.state.get(message.rid); - if (!room) { - throw new Error('Room not found'); - } - const { callStatus, _id } = room; - if (callStatus === 'declined' || callStatus === 'ended') { - dispatchToastMessage({ type: 'info', message: t('Call_Already_Ended') }); - return; - } - await sdk.rest.put(`/v1/livechat/webrtc.call/${message._id}`, { rid: _id, status: 'ended' }); - sdk.publish('notify-room', [`${_id}/webrtc`, 'callStatus', { callStatus: 'ended' }]); -}); diff --git a/apps/meteor/app/webrtc/client/adapter.ts b/apps/meteor/app/webrtc/client/adapter.ts deleted file mode 100644 index f98ae7815c051..0000000000000 --- a/apps/meteor/app/webrtc/client/adapter.ts +++ /dev/null @@ -1,7 +0,0 @@ -// FIXME: probably outdated -window.RTCPeerConnection = window.RTCPeerConnection ?? window.mozRTCPeerConnection ?? window.webkitRTCPeerConnection; -window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; -window.RTCIceCandidate = window.RTCIceCandidate ?? window.mozRTCIceCandidate ?? window.webkitRTCIceCandidate; -window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; -window.AudioContext = window.AudioContext ?? window.mozAudioContext ?? window.webkitAudioContext; -navigator.getUserMedia = navigator.getUserMedia ?? navigator.mozGetUserMedia ?? navigator.webkitGetUserMedia; diff --git a/apps/meteor/app/webrtc/client/index.ts b/apps/meteor/app/webrtc/client/index.ts deleted file mode 100644 index 0dc8337bb8c43..0000000000000 --- a/apps/meteor/app/webrtc/client/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './adapter'; -import './actionLink'; - -export * from './WebRTCClass'; diff --git a/apps/meteor/app/webrtc/client/screenShare.ts b/apps/meteor/app/webrtc/client/screenShare.ts deleted file mode 100644 index 3fac4a05bfea7..0000000000000 --- a/apps/meteor/app/webrtc/client/screenShare.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; - -export const ChromeScreenShare = { - callbacks: { - 'get-RocketChatScreenSharingExtensionVersion': (version: unknown) => { - if (version) { - ChromeScreenShare.installed = true; - } - }, - 'getSourceId': (_sourceId: string): void => undefined, - }, - installed: false, - init() { - window.postMessage('get-RocketChatScreenSharingExtensionVersion', '*'); - }, - getSourceId(navigator: string, callback: (sourceId: string) => void) { - if (!callback) { - throw new Error('"callback" parameter is mandatory.'); - } - this.callbacks.getSourceId = callback; - if (navigator === 'electron') { - return fireGlobalEvent('get-sourceId', '*'); - } - return window.postMessage('get-sourceId', '*'); - }, -}; - -ChromeScreenShare.init(); - -window.addEventListener('message', (e) => { - if (e.origin !== window.location.origin) { - return; - } - if (e.data === 'PermissionDeniedError') { - if (ChromeScreenShare.callbacks.getSourceId != null) { - return ChromeScreenShare.callbacks.getSourceId('PermissionDeniedError'); - } - throw new Error('PermissionDeniedError'); - } - if (e.data.version != null) { - ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion']?.(e.data.version); - } else if (e.data.sourceId != null) { - return typeof ChromeScreenShare.callbacks.getSourceId === 'function' && ChromeScreenShare.callbacks.getSourceId(e.data.sourceId); - } -}); diff --git a/apps/meteor/app/webrtc/lib/constants.ts b/apps/meteor/app/webrtc/lib/constants.ts deleted file mode 100644 index fd3f26933d96a..0000000000000 --- a/apps/meteor/app/webrtc/lib/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const WEB_RTC_EVENTS = { - WEB_RTC: 'webrtc', - STATUS: 'status', - CALL: 'call', - JOIN: 'join', - CANDIDATE: 'candidate', - DESCRIPTION: 'description', -} as const; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx deleted file mode 100644 index 2aa65855b5200..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import type { HTMLAttributes } from 'react'; - -import NavBarControlsMenu from './NavBarControlsMenu'; -import { useOmnichannelCallDialPadAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction'; -import { useOmnichannelCallToggleAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction'; - -type NavBarControlsMenuProps = Omit, 'is'> & { - omnichannelItems: GenericMenuItemProps[]; - isPressed: boolean; - callItem?: GenericMenuItemProps; -}; - -const NavBarControlsWithCall = ({ omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => { - const { - icon: omnichannelCallIcon, - title: omnichannelCallTitle, - handleOpenDialModal, - isDisabled: callDialPadDisabled, - } = useOmnichannelCallDialPadAction(); - - const { - title: omnichannelCallTogglerTitle, - icon: omnichannelCallTogglerIcon, - handleToggleCall, - isDisabled: callTogglerDisabled, - } = useOmnichannelCallToggleAction(); - - const omnichannelItemsWithCall = [ - ...omnichannelItems, - { - id: 'omnichannelCallDialPad', - icon: omnichannelCallIcon, - content: omnichannelCallTitle, - onClick: handleOpenDialModal, - disabled: callDialPadDisabled, - }, - { - id: 'omnichannelCallToggler', - icon: omnichannelCallTogglerIcon, - content: omnichannelCallTogglerTitle, - onClick: handleToggleCall, - disabled: callTogglerDisabled, - }, - ] as GenericMenuItemProps[]; - - return ; -}; - -export default NavBarControlsWithCall; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx deleted file mode 100644 index d1405674be8e0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useMediaCallAction } from '@rocket.chat/ui-voip'; -import type { HTMLAttributes } from 'react'; - -import NavBarControlsMenu from './NavBarControlsMenu'; -import NavbarControlsWithCall from './NavBarControlsWithCall'; -import { useIsCallEnabled } from '../../contexts/CallContext'; -import { useOmnichannelContactAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelContactAction'; -import { useOmnichannelLivechatToggle } from '../NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle'; -import { useOmnichannelQueueAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction'; - -type NavBarControlsMenuProps = Omit, 'is'>; - -const NavBarControlsWithData = (props: NavBarControlsMenuProps) => { - const isCallEnabled = useIsCallEnabled(); - - const callAction = useMediaCallAction(); - - const { - isEnabled: queueEnabled, - icon: queueIcon, - title: queueTitle, - handleGoToQueue, - isPressed: isQueuePressed, - } = useOmnichannelQueueAction(); - - const { - title: contactCenterTitle, - icon: contactCenterIcon, - handleGoToContactCenter, - isPressed: isContactPressed, - } = useOmnichannelContactAction(); - - const { - title: omnichannelLivechatTogglerTitle, - icon: omnichannelLivechatTogglerIcon, - handleAvailableStatusChange, - } = useOmnichannelLivechatToggle(); - - const callItem = callAction - ? { - id: 'rcx-media-call', - icon: callAction.icon, - content: callAction.title, - onClick: () => callAction.action(), - } - : undefined; - - const omnichannelItems = [ - queueEnabled && { - id: 'omnichannelQueue', - icon: queueIcon, - content: queueTitle, - onClick: handleGoToQueue, - }, - { - id: 'omnichannelContact', - icon: contactCenterIcon, - content: contactCenterTitle, - onClick: handleGoToContactCenter, - }, - { - id: 'omnichannelLivechatToggler', - icon: omnichannelLivechatTogglerIcon, - content: omnichannelLivechatTogglerTitle, - onClick: handleAvailableStatusChange, - }, - ].filter(Boolean) as GenericMenuItemProps[]; - - const isPressed = isQueuePressed || isContactPressed; - - if (isCallEnabled) { - return ; - } - - return ; -}; - -export default NavBarControlsWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx deleted file mode 100644 index 7206bbea812ac..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useOmnichannelCallDialPadAction } from './hooks/useOmnichannelCallDialPadAction'; - -type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; - -const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { - const { t } = useTranslation(); - - const { title, icon, handleOpenDialModal, isDisabled } = useOmnichannelCallDialPadAction(); - - return ( - - ); -}; - -export default NavBarItemOmniChannelCallDialPad; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx deleted file mode 100644 index d6589117f6fbb..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { HTMLAttributes } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useOmnichannelCallToggleAction } from './hooks/useOmnichannelCallToggleAction'; - -type NavBarItemOmnichannelCallToggleProps = Omit, 'is'>; - -const NavBarItemOmnichannelCallToggle = (props: NavBarItemOmnichannelCallToggleProps) => { - const { t } = useTranslation(); - const { icon, title, handleToggleCall, isSuccess, isWarning, isDanger, isDisabled } = useOmnichannelCallToggleAction(); - - return ( - - ); -}; - -export default NavBarItemOmnichannelCallToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx deleted file mode 100644 index a0447af49981d..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { NavBarGroup } from '@rocket.chat/fuselage'; -import { useTranslation } from 'react-i18next'; - -import NavBarItemOmniChannelCallDialPad from './NavBarItemOmniChannelCallDialPad'; -import NavBarItemOmnichannelCallToggle from './NavBarItemOmnichannelCallToggle'; -import NavBarItemOmnichannelContact from './NavBarItemOmnichannelContact'; -import NavBarItemOmnichannelLivechatToggle from './NavBarItemOmnichannelLivechatToggle'; -import NavBarItemOmnichannelQueue from './NavBarItemOmnichannelQueue'; -import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; - -const NavBarOmnichannelGroup = () => { - const { t } = useTranslation(); - - const isCallEnabled = useIsCallEnabled(); - const isCallReady = useIsCallReady(); - - return ( - - - {isCallReady && } - - {isCallEnabled && } - - - ); -}; - -export default NavBarOmnichannelGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts deleted file mode 100644 index 6cf14e43446a0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Keys } from '@rocket.chat/icons'; -import { useTranslation } from 'react-i18next'; - -import { useVoipOutboundStates } from '../../../contexts/CallContext'; -import { useDialModal } from '../../../hooks/useDialModal'; - -export const useOmnichannelCallDialPadAction = () => { - const { t } = useTranslation(); - - const { openDialModal } = useDialModal(); - - const { outBoundCallsAllowed, outBoundCallsEnabledForUser } = useVoipOutboundStates(); - - return { - isDisabled: !outBoundCallsEnabledForUser, - handleOpenDialModal: () => openDialModal(), - icon: 'dialpad' as Keys, - title: outBoundCallsAllowed ? t('New_Call') : t('New_Call_Premium_Only'), - }; -}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts deleted file mode 100644 index a58f3de3cebe4..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Keys as IconName } from '@rocket.chat/icons'; -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - useIsCallReady, - useIsCallError, - useCallerInfo, - useCallRegisterClient, - useCallUnregisterClient, - useVoipNetworkStatus, -} from '../../../contexts/CallContext'; - -export const useOmnichannelCallToggleAction = () => { - const { t } = useTranslation(); - const isCallReady = useIsCallReady(); - const isCallError = useIsCallError(); - - const caller = useCallerInfo(); - const unregister = useCallUnregisterClient(); - const register = useCallRegisterClient(); - - const networkStatus = useVoipNetworkStatus(); - const registered = !['ERROR', 'INITIAL', 'UNREGISTERED'].includes(caller.state); - const inCall = ['IN_CALL'].includes(caller.state); - - const handleToggleCall = useCallback(() => { - if (registered) { - unregister(); - return; - } - register(); - }, [registered, register, unregister]); - - const title = useMemo(() => { - if (isCallError) { - return t('Error'); - } - - if (!isCallReady) { - return t('Loading'); - } - - if (networkStatus === 'offline') { - return t('Waiting_for_server_connection'); - } - - if (inCall) { - return t('Cannot_disable_while_on_call'); - } - - if (registered) { - return t('Turn_off_answer_calls'); - } - - return t('Turn_on_answer_calls'); - }, [inCall, isCallError, isCallReady, networkStatus, registered, t]); - - const icon: IconName = useMemo(() => { - if (networkStatus === 'offline') { - return 'phone-issue'; - } - return registered ? 'phone' : 'phone-disabled'; - }, [networkStatus, registered]); - - return { - handleToggleCall, - title, - icon, - isDisabled: inCall || isCallError || !isCallReady, - isDanger: isCallError, - isSuccess: registered, - isWarning: networkStatus === 'offline', - }; -}; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx deleted file mode 100644 index 81603cdeeca54..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { testCreateChannelModal } from './testCreateChannelModal'; -import CreateChannelModalComponent from '../../../sidebar/header/CreateChannel'; - -jest.mock('../../../lib/utils/goToRoomById', () => ({ - goToRoomById: jest.fn(), -})); - -testCreateChannelModal(CreateChannelModalComponent); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx deleted file mode 100644 index 2ccdc1321a502..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import { - Box, - Modal, - Button, - TextInput, - Icon, - Field, - ToggleSwitch, - FieldGroup, - FieldLabel, - FieldRow, - FieldError, - FieldHint, - Accordion, - AccordionItem, - ModalHeader, - ModalTitle, - ModalClose, - ModalContent, - ModalFooter, - ModalFooterControllers, -} from '@rocket.chat/fuselage'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useTranslation, useEndpoint, useToastMessageDispatch, usePermissionWithScopedRoles } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, ReactElement } from 'react'; -import { useId, useEffect, useMemo } from 'react'; -import { useForm, Controller } from 'react-hook-form'; - -import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; -import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; -import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; -import { useIsFederationEnabled } from '../../../hooks/useIsFederationEnabled'; -import { goToRoomById } from '../../../lib/utils/goToRoomById'; - -type CreateChannelModalProps = { - teamId?: string; - onClose: () => void; - reload?: () => void; -}; - -type CreateChannelModalPayload = { - name: string; - isPrivate: boolean; - topic?: string; - members: string[]; - readOnly: boolean; - encrypted: boolean; - broadcast: boolean; - federated: boolean; -}; - -const getFederationHintKey = (licenseModule: ReturnType, featureToggle: boolean): TranslationKey => { - if (licenseModule === 'loading' || !licenseModule) { - return 'error-this-is-a-premium-feature'; - } - if (!featureToggle) { - return 'Federation_Matrix_Federated_Description_disabled'; - } - return 'Federation_Matrix_Federated_Description'; -}; - -const hasExternalMembers = (members: string[]): boolean => members.some((member) => member.startsWith('@')); - -const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModalProps) => { - const t = useTranslation(); - const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); - const e2eEnabled = useSetting('E2E_Enable'); - const namesValidation = useSetting('UTF8_Channel_Names_Validation'); - const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); - const federationEnabled = useIsFederationEnabled(); - const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; - - const getEncryptedHint = useEncryptedRoomDescription('channel'); - - const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); - const federatedModule = useHasLicenseModule('federation'); - const canUseFederation = federatedModule !== 'loading' && federatedModule && federationEnabled; - - const channelNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); - const createChannel = useEndpoint('POST', '/v1/channels.create'); - const createPrivateChannel = useEndpoint('POST', '/v1/groups.create'); - - const dispatchToastMessage = useToastMessageDispatch(); - - const canOnlyCreateOneType = useCreateChannelTypePermission(); - - const { - register, - formState: { errors }, - handleSubmit, - control, - setValue, - watch, - } = useForm({ - mode: 'onBlur', - defaultValues: { - members: [], - name: '', - topic: '', - isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, - readOnly: false, - encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, - broadcast: false, - federated: false, - }, - }); - - const { isPrivate, broadcast, readOnly, federated, encrypted } = watch(); - - useEffect(() => { - if (federated) { - // if room is federated, it cannot be encrypted or broadcast or readOnly - setValue('encrypted', false); - setValue('broadcast', false); - setValue('readOnly', false); - } - }, [federated, setValue]); - - useEffect(() => { - if (!isPrivate) { - setValue('encrypted', false); - } - }, [isPrivate, setValue]); - - useEffect(() => { - setValue('readOnly', broadcast); - }, [broadcast, setValue]); - - const validateChannelName = async (name: string): Promise => { - if (!name) { - return; - } - - if (!allowSpecialNames && !channelNameRegex.test(name)) { - return t('Name_cannot_have_special_characters'); - } - - const { exists } = await channelNameExists({ roomName: name }); - if (exists) { - return t('Channel_already_exist', name); - } - }; - - const handleCreateChannel = async ({ name, members, readOnly, topic, broadcast, encrypted, federated }: CreateChannelModalPayload) => { - let roomData; - const params = { - name, - members, - readOnly, - extraData: { - topic, - broadcast, - encrypted, - ...(federated && { federated }), - ...(teamId && { teamId }), - }, - }; - - try { - if (isPrivate) { - roomData = await createPrivateChannel(params); - !teamId && goToRoomById(roomData.group._id); - } else { - roomData = await createChannel(params); - !teamId && goToRoomById(roomData.channel._id); - } - - dispatchToastMessage({ type: 'success', message: t('Room_has_been_created') }); - reload?.(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - onClose(); - } - }; - - const e2eDisabled = useMemo(() => !isPrivate || Boolean(!e2eEnabled) || federated, [e2eEnabled, federated, isPrivate]); - - const createChannelFormId = useId(); - const nameId = useId(); - const topicId = useId(); - const privateId = useId(); - const federatedId = useId(); - const readOnlyId = useId(); - const encryptedId = useId(); - const broadcastId = useId(); - const addMembersId = useId(); - - return ( - ) => ( - - )} - > - - {t('Create_channel')} - - - - - - - {t('Name')} - - - validateChannelName(value), - })} - error={errors.name?.message} - addon={} - aria-invalid={errors.name ? 'true' : 'false'} - aria-describedby={`${nameId}-error ${nameId}-hint`} - aria-required='true' - /> - - {errors.name && ( - - {errors.name.message} - - )} - {!allowSpecialNames && {t('No_spaces_or_special_characters')}} - - - {t('Topic')} - - - - {t('Displayed_next_to_name')} - - - {t('Members')} - - !federated && hasExternalMembers(members) ? t('You_cannot_add_external_users_to_non_federated_room') : true, - }} - render={({ field: { onChange, value } }): ReactElement => ( - - )} - /> - {errors.members && ( - - {errors.members.message} - - )} - - - - {t('Private')} - ( - - )} - /> - - - {isPrivate ? t('People_can_only_join_by_being_invited') : t('Anyone_can_access')} - - - - - - - - {t('Security_and_permissions')} - - - - - {t('Federation_Matrix_Federated')} - - ( - - )} - /> - - {t(getFederationHintKey(federatedModule, federationEnabled))} - - - - - {t('Encrypted')} - - ( - - )} - /> - - {getEncryptedHint({ isPrivate, encrypted })} - - - - - {t('Read_only')} - - ( - - )} - /> - - - {readOnly ? t('Read_only_field_hint_enabled', { roomType: 'channel' }) : t('Anyone_can_send_new_messages')} - - - - - - {t('Broadcast')} - - ( - - )} - /> - - {broadcast && {t('Broadcast_hint_enabled', { roomType: 'channel' })}} - - - - - - - - - - - - - ); -}; - -export default CreateChannelModal; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx deleted file mode 100644 index 2382eee8cafc8..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { - Box, - Modal, - Button, - FieldGroup, - Field, - FieldRow, - FieldError, - FieldHint, - ModalHeader, - ModalTitle, - ModalClose, - ModalContent, - ModalFooter, - ModalFooterControllers, -} from '@rocket.chat/fuselage'; -import { useTranslation, useEndpoint, useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts'; -import { useMutation } from '@tanstack/react-query'; -import { useId, memo } from 'react'; -import { useForm, Controller } from 'react-hook-form'; - -import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { goToRoomById } from '../../../lib/utils/goToRoomById'; - -type CreateDirectMessageProps = { onClose: () => void }; - -const CreateDirectMessage = ({ onClose }: CreateDirectMessageProps) => { - const t = useTranslation(); - const directMaxUsers = useSetting('DirectMesssage_maxUsers', 1); - const membersFieldId = useId(); - const dispatchToastMessage = useToastMessageDispatch(); - - const createDirectAction = useEndpoint('POST', '/v1/dm.create'); - - const { - control, - handleSubmit, - formState: { 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(); - }, - }); - - const handleCreate = async ({ users }: { users: IUser['username'][] }) => { - return mutateDirectMessage.mutateAsync({ usernames: users.join(',') }); - }; - - return ( - }> - - {t('Create_direct_message')} - - - - - - {t('Direct_message_creation_description')} - - - users.length + 1 > directMaxUsers - ? t('error-direct-message-max-user-exceeded', { maxUsers: directMaxUsers }) - : undefined, - }} - control={control} - render={({ field: { name, onChange, value, onBlur } }) => ( - - )} - /> - - {errors.users && ( - - {errors.users.message} - - )} - {t('Direct_message_creation_description_hint')} - - - - - - - - - - - ); -}; - -export default memo(CreateDirectMessage); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.spec.tsx deleted file mode 100644 index 8e7c4a3a5bad5..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.spec.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { mockAppRoot } from '@rocket.chat/mock-providers'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import CreateTeamModal from './CreateTeamModal'; -import CreateTeamModalOld from '../../../sidebar/header/CreateTeam'; - -jest.mock('../../../lib/utils/goToRoomById', () => ({ - goToRoomById: jest.fn(), -})); - -type CreateTeamModalComponentType = typeof CreateTeamModal | typeof CreateTeamModalOld; -// eslint-disable-next-line @typescript-eslint/naming-convention - -describe.each([ - ['CreateTeamModal', CreateTeamModalOld], - ['CreateTeamModal in NavbarV2', CreateTeamModal], -] as const)( - '%s', - // eslint-disable-next-line @typescript-eslint/naming-convention - (_name: string, CreateTeamModalComponent: CreateTeamModalComponentType) => { - it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('should render with encryption option enabled and set to off when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=false', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=true', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('should render with encryption option enabled and set to on when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=True', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('when Private goes ON → OFF: forces Encrypted OFF and disables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement; - - // initial: private=true, encrypted ON and enabled - expect(priv).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // Private ON -> OFF: encrypted must become OFF and disabled - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('when Private goes OFF → ON: keeps Encrypted OFF but re-enables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement; - - // turn private OFF to simulate user path from non-private - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // turn private back ON -> encrypted should remain OFF but become enabled - await userEvent.click(priv); - expect(priv).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('private team: toggling Broadcast on/off does not change or disable Encrypted', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement; - - expect(priv).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - expect(broadcast).not.toBeChecked(); - - // Broadcast: OFF -> ON (Encrypted unchanged + enabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // Broadcast: ON -> OFF (Encrypted unchanged + enabled) - await userEvent.click(broadcast); - expect(broadcast).not.toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // User can still toggle Encrypted freely while Broadcast is OFF - await userEvent.click(encrypted); - expect(encrypted).not.toBeChecked(); - - // User can still toggle Encrypted freely while Broadcast is ON - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('non-private team: Encrypted remains OFF and disabled regardless of Broadcast state', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement; - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement; - - // Switch to non-private - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - - // Encrypted must be OFF + disabled (non-private cannot be encrypted) - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // Broadcast: OFF -> ON (Encrypted stays OFF + disabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // Broadcast: ON -> OFF (Encrypted still OFF + disabled) - await userEvent.click(broadcast); - expect(broadcast).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('should disable and turn on ReadOnly toggle when Broadcast is ON and no set-readonly permission', async () => { - render( null} />, { - wrapper: mockAppRoot().build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement; - - expect(readOnly).not.toBeChecked(); - - // Broadcast: OFF -> ON (ReadOnly stays ON + disabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(readOnly).toBeChecked(); - expect(readOnly).toBeDisabled(); - }); - - it('should disable and turn on ReadOnly toggle when Broadcast is ON with set-readonly permission', async () => { - render( null} />, { - wrapper: mockAppRoot().withPermission('set-readonly').build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement; - - expect(readOnly).not.toBeChecked(); - - // Broadcast: OFF -> ON (ReadOnly stays ON + disabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(readOnly).toBeChecked(); - expect(readOnly).toBeDisabled(); - }); - - it('should disable and turn off ReadOnly toggle when Broadcast is OFF with no set-readonly permission', async () => { - render( null} />, { - wrapper: mockAppRoot().build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement; - - expect(broadcast).not.toBeChecked(); - expect(readOnly).not.toBeChecked(); - expect(readOnly).toBeDisabled(); - }); - - it('should enable ReadOnly toggle when Broadcast is OFF with set-readonly permission', async () => { - render( null} />, { - wrapper: mockAppRoot().withPermission('set-readonly').build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement; - const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement; - - expect(broadcast).not.toBeChecked(); - expect(readOnly).not.toBeChecked(); - expect(readOnly).toBeEnabled(); - }); - }, -); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/testCreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/testCreateChannelModal.tsx deleted file mode 100644 index e2971bd6fa36b..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/testCreateChannelModal.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { mockAppRoot } from '@rocket.chat/mock-providers'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import type CreateChannelModal2Component from './CreateChannelModal'; -import type CreateChannelModalComponent from '../../../sidebar/header/CreateChannel'; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function testCreateChannelModal(CreateChannelModal: typeof CreateChannelModalComponent | typeof CreateChannelModal2Component) { - describe('CreateChannelModal', () => { - it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('should render with encryption option enabled and set to off when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=false', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=true', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - expect(encrypted).toBeInTheDocument(); - - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('should render with encryption option enabled and set to on when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=True', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('when Private goes ON → OFF: forces Encrypted OFF and disables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - const priv = screen.getByLabelText('Private') as HTMLInputElement; - - // initial: private=true, encrypted ON and enabled - expect(priv).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // Private ON -> OFF: encrypted must become OFF and disabled - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - - it('when Private goes OFF → ON: keeps Encrypted OFF but re-enables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - const priv = screen.getByLabelText('Private') as HTMLInputElement; - - // turn private OFF to simulate user path from non-private - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // turn private back ON -> encrypted should remain OFF but become enabled - await userEvent.click(priv); - expect(priv).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('private room: toggling Broadcast on/off does not change or disable Encrypted', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - const broadcast = screen.getByLabelText('Broadcast') as HTMLInputElement; - const priv = screen.getByLabelText('Private') as HTMLInputElement; - - expect(priv).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - expect(broadcast).not.toBeChecked(); - - // Broadcast: OFF -> ON (Encrypted unchanged + enabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // Broadcast: ON -> OFF (Encrypted unchanged + enabled) - await userEvent.click(broadcast); - expect(broadcast).not.toBeChecked(); - expect(encrypted).toBeChecked(); - expect(encrypted).toBeEnabled(); - - // User can still toggle Encrypted freely while Broadcast is OFF - await userEvent.click(encrypted); - expect(encrypted).not.toBeChecked(); - - // User can still toggle Encrypted freely while Broadcast is ON - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeEnabled(); - }); - - it('non-private room: Encrypted remains OFF and disabled regardless of Broadcast state', async () => { - render( null} />, { - wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), - }); - - await userEvent.click(screen.getByText('Advanced_settings')); - - const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; - const broadcast = screen.getByLabelText('Broadcast') as HTMLInputElement; - const priv = screen.getByLabelText('Private') as HTMLInputElement; - - // Switch to non-private - await userEvent.click(priv); - expect(priv).not.toBeChecked(); - - // Encrypted must be OFF + disabled (non-private cannot be encrypted) - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // Broadcast: OFF -> ON (Encrypted stays OFF + disabled) - await userEvent.click(broadcast); - expect(broadcast).toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - - // Broadcast: ON -> OFF (Encrypted still OFF + disabled) - await userEvent.click(broadcast); - expect(broadcast).not.toBeChecked(); - expect(encrypted).not.toBeChecked(); - expect(encrypted).toBeDisabled(); - }); - }); -} diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts deleted file mode 100644 index 2a90219e03fd7..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useTranslation } from 'react-i18next'; - -import { useCreateRoomModal } from './useCreateRoomModal'; -import MatrixFederationSearch from '../../../sidebarv2/header/MatrixFederationSearch'; - -export const useMatrixFederationItems = ({ - isMatrixEnabled, -}: { - isMatrixEnabled: string | number | boolean | null | undefined; -}): GenericMenuItemProps[] => { - const { t } = useTranslation(); - - const searchFederatedRooms = useCreateRoomModal(MatrixFederationSearch); - - const matrixFederationSearchItem: GenericMenuItemProps = { - id: 'matrix-federation-search', - content: t('Federation_Search_federated_rooms'), - icon: 'magnifier', - onClick: () => { - searchFederatedRooms(); - }, - }; - - return [...(isMatrixEnabled ? [matrixFederationSearchItem] : [])]; -}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx deleted file mode 100644 index da83a9ca1c129..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage'; -import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; - -import NavBarSearchItem from './NavBarSearchItem'; -import { RoomIcon } from '../../components/RoomIcon'; -import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; -import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges'; -import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay'; - -type NavBarSearchItemWithDataProps = { - room: SubscriptionWithRoom; - id: string; - AvatarTemplate: ReactElement; -} & Partial>; - -const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSearchItemWithDataProps) => { - const { t } = useTranslation(); - - const href = roomCoordinator.getRouteLink(room.t, room) || ''; - const title = roomCoordinator.getRoomName(room.t, room) || ''; - - const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room); - - const icon = } />; - - const badges = ( - <> - {showUnread && ( - - {unreadCount.total} - - )} - {isOmnichannelRoom(room) && } - - ); - - return ( - - ); -}; - -export default NavBarSearchItemWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx deleted file mode 100644 index ee6bc5bfcf17c..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { - Field, - TextInput, - FieldGroup, - Modal, - Button, - Box, - FieldLabel, - FieldRow, - FieldError, - FieldHint, - ModalHeader, - ModalIcon, - ModalTitle, - ModalClose, - ModalContent, - ModalFooter, - ModalFooterControllers, -} from '@rocket.chat/fuselage'; -import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useSetting, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ChangeEvent, ComponentProps, FormEvent } from 'react'; -import { useState, useCallback } from 'react'; - -import UserStatusMenu from '../../../components/UserStatusMenu'; -import { USER_STATUS_TEXT_MAX_LENGTH } from '../../../lib/constants'; - -type EditStatusModalProps = { - onClose: () => void; - userStatus: IUser['status']; - userStatusText: IUser['statusText']; -}; - -const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModalProps): ReactElement => { - const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange'); - const dispatchToastMessage = useToastMessageDispatch(); - const [customStatus, setCustomStatus] = useLocalStorage('Local_Custom_Status', ''); - const initialStatusText = customStatus || userStatusText; - - const t = useTranslation(); - const [statusText, setStatusText] = useState(initialStatusText); - const [statusType, setStatusType] = useState(userStatus); - const [statusTextError, setStatusTextError] = useState(); - - const setUserStatus = useEndpoint('POST', '/v1/users.setStatus'); - - const handleStatusText = useEffectEvent((e: ChangeEvent): void => { - setStatusText(e.currentTarget.value); - - if (statusText && statusText.length > USER_STATUS_TEXT_MAX_LENGTH) { - return setStatusTextError(t('Max_length_is', USER_STATUS_TEXT_MAX_LENGTH)); - } - - return setStatusTextError(undefined); - }); - - const handleStatusType = (type: IUser['status']): void => setStatusType(type); - - const handleSaveStatus = useCallback(async () => { - try { - await setUserStatus({ message: statusText, status: statusType }); - setCustomStatus(statusText); - dispatchToastMessage({ type: 'success', message: t('StatusMessage_Changed_Successfully') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - - onClose(); - }, [onClose, setUserStatus, statusText, statusType, setCustomStatus, dispatchToastMessage, t]); - - return ( - ) => ( - { - e.preventDefault(); - handleSaveStatus(); - }} - {...props} - /> - )} - > - - - {t('Edit_Status')} - - - - - - {t('StatusMessage')} - - } - /> - - {!allowUserStatusMessageChange && {t('StatusMessage_Change_Disabled')}} - {statusTextError} - - - - - - - - - - - ); -}; - -export default EditStatusModal; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx deleted file mode 100644 index a965e94002e82..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useCustomStatusModalHandler } from './useCustomStatusModalHandler'; -import { callbacks } from '../../../../../lib/callbacks'; -import MarkdownText from '../../../../components/MarkdownText'; -import { UserStatus } from '../../../../components/UserStatus'; -import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent'; -import { userStatuses } from '../../../../lib/userStatuses'; -import type { UserStatusDescriptor } from '../../../../lib/userStatuses'; -import { useStatusDisabledModal } from '../../../../views/admin/customUserStatus/hooks/useStatusDisabledModal'; - -export const useStatusItems = (): GenericMenuItemProps[] => { - // We should lift this up to somewhere else if we want to use it in other places - - userStatuses.invisibleAllowed = useSetting('Accounts_AllowInvisibleStatusOption', true); - - const queryClient = useQueryClient(); - - useEffect( - () => - userStatuses.watch(() => { - queryClient.setQueryData(['user-statuses'], Array.from(userStatuses)); - }), - [queryClient], - ); - - const { t } = useTranslation(); - - const fireGlobalStatusEvent = useFireGlobalEvent('user-status-manually-set'); - const setStatus = useEndpoint('POST', '/v1/users.setStatus'); - const setStatusMutation = useMutation({ - mutationFn: async (status: UserStatusDescriptor) => { - void setStatus({ status: status.statusType, message: userStatuses.isValidType(status.id) ? '' : status.name }); - void callbacks.run('userStatusManuallySet', status); - await fireGlobalStatusEvent.mutateAsync(status); - }, - }); - - const presenceDisabled = useSetting('Presence_broadcast_disabled', false); - - const { data: statuses } = useQuery({ - queryKey: ['user-statuses'], - queryFn: async () => { - await userStatuses.sync(); - return Array.from(userStatuses); - }, - staleTime: Infinity, - select: (statuses) => - statuses.map((status): GenericMenuItemProps => { - const content = status.localizeName ? t(status.name) : status.name; - return { - id: status.id, - status: , - content: , - disabled: presenceDisabled, - onClick: () => setStatusMutation.mutate(status), - }; - }), - }); - - const handleStatusDisabledModal = useStatusDisabledModal(); - const handleCustomStatus = useCustomStatusModalHandler(); - - return [ - ...(presenceDisabled - ? [ - { - id: 'presence-disabled', - content: ( - - - {t('User_status_disabled')} - - - {t('Learn_more')} - - - ), - }, - ] - : []), - ...(statuses ?? []), - { id: 'custom-status', icon: 'emoji', content: t('Custom_Status'), onClick: handleCustomStatus, disabled: presenceDisabled }, - ]; -}; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx deleted file mode 100644 index 744513c6316e0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import { useTranslation } from 'react-i18next'; - -import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; - -export const useAuditMenu = () => { - const router = useRouter(); - const { t } = useTranslation(); - - const hasAuditLicense = useHasLicenseModule('auditing') === true; - - const hasAuditPermission = usePermission('can-audit') && hasAuditLicense; - const hasAuditLogPermission = usePermission('can-audit-log') && hasAuditLicense; - - const auditMessageItem: GenericMenuItemProps = { - id: 'messages', - content: t('Messages'), - onClick: () => router.navigate('/audit'), - }; - - const auditLogItem: GenericMenuItemProps = { - id: 'auditLog', - content: t('Logs'), - onClick: () => router.navigate('/audit-log'), - }; - - const auditSecurityLogsItem: GenericMenuItemProps = { - id: 'auditSecurityLog', - content: t('Security_logs'), - onClick: () => router.navigate('/security-logs'), - }; - - return { - title: t('Audit'), - items: [ - hasAuditPermission && auditMessageItem, - hasAuditLogPermission && auditLogItem, - hasAuditPermission && auditSecurityLogsItem, - ].filter(Boolean) as GenericMenuItemProps[], - }; -}; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx deleted file mode 100644 index 7635effb1b4d0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { NavBarGroup, NavBarItem } from '@rocket.chat/fuselage'; -import { useMediaCallAction } from '@rocket.chat/ui-voip'; -import { useTranslation } from 'react-i18next'; - -const NavBarVoipGroup = () => { - const { t } = useTranslation(); - - const callAction = useMediaCallAction(); - if (!callAction) { - return null; - } - - return ( - - callAction.action()} /> - - ); -}; - -export default NavBarVoipGroup; diff --git a/apps/meteor/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/client/apps/gameCenter/GameCenter.tsx index 2fc771d781100..058cc7dfdcd49 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenter.tsx @@ -1,5 +1,6 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useRoomToolbox } from '@rocket.chat/ui-contexts'; import { useState } from 'react'; import type { MouseEvent, ReactElement } from 'react'; @@ -7,7 +8,6 @@ import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; import { useExternalComponentsQuery } from './hooks/useExternalComponentsQuery'; import { preventSyntheticEvent } from '../../lib/utils/preventSyntheticEvent'; -import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; export type IGame = IExternalComponent; diff --git a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index 6ff1f333c50b4..a275e98107a92 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -1,8 +1,4 @@ import { Avatar } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; - -import type { IGame } from './GameCenter'; import { ContextualbarTitle, ContextualbarHeader, @@ -10,7 +6,11 @@ import { ContextualbarContent, ContextualbarClose, ContextualbarDialog, -} from '../../components/Contextualbar'; +} from '@rocket.chat/ui-client'; +import type { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { IGame } from './GameCenter'; interface IGameCenterContainerProps { handleClose: () => void; diff --git a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index a7eb7b2896be7..1bbf634a0f8d5 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { IGame } from './GameCenter'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple'; import { useOpenedRoom } from '../../lib/RoomManager'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling'; @@ -57,7 +57,7 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer {t('Invite_Users')} - + diff --git a/apps/meteor/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx index 1e9e1bb2b5c37..539a4f20b4a92 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx @@ -1,11 +1,4 @@ import { Avatar, Icon, Table, TableBody, TableCell, TableHead, TableRow } from '@rocket.chat/fuselage'; -import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import type { IGame } from './GameCenter'; -import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; import { ContextualbarHeader, ContextualbarTitle, @@ -13,7 +6,14 @@ import { ContextualbarContent, ContextualbarDialog, ContextualbarSkeleton, -} from '../../components/Contextualbar'; +} from '@rocket.chat/ui-client'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { IGame } from './GameCenter'; +import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; interface IGameCenterListProps { handleClose: () => void; diff --git a/apps/meteor/client/cachedStores/RoomsCachedStore.ts b/apps/meteor/client/cachedStores/RoomsCachedStore.ts index 6ab8c929e73b0..f5ee93b9701b7 100644 --- a/apps/meteor/client/cachedStores/RoomsCachedStore.ts +++ b/apps/meteor/client/cachedStores/RoomsCachedStore.ts @@ -103,7 +103,7 @@ class RoomsCachedStore extends PrivateCachedStore { ); } - protected deserializeFromCache(record: unknown) { + protected override deserializeFromCache(record: unknown) { const deserialized = super.deserializeFromCache(record); if (deserialized?.lastMessage?._updatedAt) { diff --git a/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts index 6e5fea6c2a488..99ff7f9c0b012 100644 --- a/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts +++ b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts @@ -79,7 +79,7 @@ class SubscriptionsCachedStore extends PrivateCachedStore ({ - useHasLicenseModule: jest.fn(() => false), -})); +import { createFakeLicenseInfo } from '../../../../tests/mocks/data'; jest.mock('../../GenericUpsellModal/hooks', () => ({ useUpsellActions: jest.fn(() => ({ @@ -34,6 +30,9 @@ const appRoot = mockAppRoot() Upgrade: 'Upgrade', Cancel: 'Cancel', }) + .withEndpoint('GET', '/v1/licenses.info', async () => ({ + license: createFakeLicenseInfo(), + })) .build(); describe('ABACUpsellModal', () => { diff --git a/apps/meteor/client/components/ABAC/ABACUpsellModal/__snapshots__/ABACUpsellModal.spec.tsx.snap b/apps/meteor/client/components/ABAC/ABACUpsellModal/__snapshots__/ABACUpsellModal.spec.tsx.snap index 51209c5122adc..d3afc0672db5e 100644 --- a/apps/meteor/client/components/ABAC/ABACUpsellModal/__snapshots__/ABACUpsellModal.spec.tsx.snap +++ b/apps/meteor/client/components/ABAC/ABACUpsellModal/__snapshots__/ABACUpsellModal.spec.tsx.snap @@ -10,7 +10,7 @@ exports[`ABACUpsellModal should render the modal with correct content 1`] = ` open="" >
Premium capability
diff --git a/apps/meteor/client/components/Contextualbar/Contextualbar.tsx b/apps/meteor/client/components/Contextualbar/Contextualbar.tsx deleted file mode 100644 index 7325fe129b342..0000000000000 --- a/apps/meteor/client/components/Contextualbar/Contextualbar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ContextualbarV2, Contextualbar as ContextualbarComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const Contextualbar = forwardRef>(function Contextualbar(props, ref) { - return ( - - - - - - - - - ); -}); - -export default memo(Contextualbar); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarAction.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarAction.tsx deleted file mode 100644 index a8d0fbd04e61f..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarAction.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ContextualbarAction as ContextualbarActionComponent, ContextualbarV2Action } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const ContextualbarAction = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(ContextualbarAction); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarActions.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarActions.tsx deleted file mode 100644 index a90626827cdee..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarActions.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ContextualbarV2Actions, ContextualbarActions as ContextualbarActionsComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const ContextualbarActions = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(ContextualbarActions); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarButton.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarButton.tsx deleted file mode 100644 index 62562f265cb4c..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ContextualbarV2Button, ContextualbarButton as ContextualbarButtonComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const ContextualbarButton = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(ContextualbarButton); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarContent.tsx deleted file mode 100644 index ebaecc789725e..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarContent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ContextualbarV2Content, ContextualbarContent as ContextualbarContentComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const ContextualbarContent = forwardRef>( - function ContextualbarContent(props, ref) { - return ( - - - - - - - - - ); - }, -); - -export default memo(ContextualbarContent); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx deleted file mode 100644 index 26afa86a22acd..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import { useLayoutSizes, useLayoutContextualBarPosition } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; -import { useCallback, useRef } from 'react'; -import type { AriaDialogProps } from 'react-aria'; -import { FocusScope, useDialog } from 'react-aria'; - -import Contextualbar from './Contextualbar'; -import ContextualbarResizable from './ContextualbarResizable'; -import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; - -type ContextualbarDialogProps = AriaDialogProps & ComponentProps & { onClose?: () => void }; - -/** - * @prop onClose can be used to close contextualbar outside the room context with ESC key - * */ -const ContextualbarDialog = ({ onClose, ...props }: ContextualbarDialogProps) => { - const ref = useRef(null); - const { dialogProps } = useDialog({ 'aria-labelledby': 'contextualbarTitle', ...props }, ref); - const { contextualBar } = useLayoutSizes(); - const position = useLayoutContextualBarPosition(); - const { closeTab } = useRoomToolbox(); - const closeContextualbar = onClose ?? closeTab; - - const callbackRef = useCallback( - (node: HTMLElement | null) => { - if (!node) { - return; - } - - ref.current = node; - node.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Escape') { - closeContextualbar(); - } - }); - }, - [closeContextualbar], - ); - - return ( - - - - - - - - - - - - - ); -}; - -export default ContextualbarDialog; diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarEmptyContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarEmptyContent.tsx deleted file mode 100644 index 63be25a395e99..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarEmptyContent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ContextualbarV2EmptyContent, ContextualbarEmptyContent as ContextualbarEmptyContentComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const ContextualbarEmptyContent = forwardRef>( - function ContextualbarEmptyContent(props, ref) { - return ( - - - - - - - - - ); - }, -); - -export default memo(ContextualbarEmptyContent); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarFooter.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarFooter.tsx deleted file mode 100644 index b8f6c60265812..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarFooter.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ContextualbarV2Footer, ContextualbarFooter as ContextualbarFooterComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const ContextualbarFooter = forwardRef>( - function ContextualbarFooter(props, ref) { - return ( - - - - - - - - - ); - }, -); - -export default memo(ContextualbarFooter); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx deleted file mode 100644 index 5d289b18dbdae..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ContextualbarV2Header, ContextualbarHeader as ContextualbarHeaderComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentPropsWithoutRef, ReactNode } from 'react'; -import { memo } from 'react'; - -type ContextualbarHeaderProps = { - expanded?: boolean; - children: ReactNode; -} & ComponentPropsWithoutRef; - -const ContextualbarHeader = ({ expanded, ...props }: ContextualbarHeaderProps) => ( - - - - - - - - -); - -export default memo(ContextualbarHeader); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarIcon.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarIcon.tsx deleted file mode 100644 index 488e4e536b89b..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ContextualbarV2Icon, ContextualbarIcon as ContextualbarIconComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const ContextualbarIcon = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(ContextualbarIcon); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx deleted file mode 100644 index 4e4bb9e0c44db..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Margins } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -import { PageScrollableContent } from '../Page'; - -const ContextualbarScrollableContent = forwardRef>( - function ContextualbarScrollableContent({ children, ...props }, ref) { - return ( - - - - {children} - - - - - {children} - - - - ); - }, -); - -export default memo(ContextualbarScrollableContent); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarSection.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarSection.tsx deleted file mode 100644 index 0226d214f8257..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarSection.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ContextualbarV2Section, ContextualbarSection as ContextualbarSectionComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const ContextualbarSection = forwardRef>( - function ContextualbarSection(props, ref) { - return ( - - - - - - - - - ); - }, -); - -export default memo(ContextualbarSection); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarTitle.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarTitle.tsx deleted file mode 100644 index d0570641de39d..0000000000000 --- a/apps/meteor/client/components/Contextualbar/ContextualbarTitle.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ContextualbarV2Title, ContextualbarTitle as ContextualbarTitleComponent } from '@rocket.chat/fuselage'; -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; - -const ContextualbarTitle = (props: ComponentProps) => ( - - - - - - - - -); - -export default ContextualbarTitle; diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index 438efbcd3e957..7aae6736909b5 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -27,10 +27,10 @@ import type { ReactElement } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { goToRoomById } from '../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from '../../navbar/NavBarPagesGroup/actions/useEncryptedRoomDescription'; import RoomAutoComplete from '../RoomAutoComplete'; import UserAutoCompleteMultiple from '../UserAutoCompleteMultiple'; import DefaultParentRoomField from './DefaultParentRoomField'; -import { useEncryptedRoomDescription } from '../../NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription'; type CreateDiscussionFormValues = { name: string; diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index e583604d0544e..353193e50a337 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -21,8 +21,8 @@ const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintCha onConfirm={onConfirm} onClose={onClose} onCancel={onCancel} - confirmText={t('New_workspace')} - cancelText={t('Configuration_update')} + confirmText={t('Configuration_update')} + cancelText={t('New_workspace')} > { - const enableTimestamp = useFeaturePreview('enable-timestamp-message-parser'); const [userLanguage] = useLocalStorage('userLanguage', 'en'); const highlights = useMessageListHighlights(); @@ -137,7 +134,6 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe ownUserId, showMentionSymbol, triggerProps, - enableTimestamp, language: userLanguage, }} > diff --git a/apps/meteor/client/components/GenericNoResults/GenericNoResults.stories.tsx b/apps/meteor/client/components/GenericNoResults/GenericNoResults.stories.tsx new file mode 100644 index 0000000000000..e26a23cacf4b1 --- /dev/null +++ b/apps/meteor/client/components/GenericNoResults/GenericNoResults.stories.tsx @@ -0,0 +1,30 @@ +import { TextInput, Box, Icon } from '@rocket.chat/fuselage'; +import type { Meta, StoryFn } from '@storybook/react'; + +import GenericNoResults from './GenericNoResults'; + +export default { + component: GenericNoResults, + parameters: { + layout: 'padded', + actions: { argTypesRegex: '^on.*' }, + }, + decorators: [ + (fn) =>
{fn()}
, + ], +} satisfies Meta; + +const filter = ( + <> + + } /> + + +); + +export const NoResults: StoryFn = () => ( + <> + {filter} + + +); diff --git a/apps/meteor/client/components/GenericTable/index.ts b/apps/meteor/client/components/GenericTable/index.ts deleted file mode 100644 index 29855bb106e33..0000000000000 --- a/apps/meteor/client/components/GenericTable/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './GenericTable'; -export * from './GenericTableBody'; -export * from './GenericTableCell'; -export * from './GenericTableHeader'; -export * from './GenericTableHeaderCell'; -export * from './GenericTableLoadingRow'; -export * from './GenericTableLoadingTable'; -export * from './GenericTableRow'; diff --git a/apps/meteor/client/components/GenericUpsellModal/__snapshots__/GenericUpsellModal.spec.tsx.snap b/apps/meteor/client/components/GenericUpsellModal/__snapshots__/GenericUpsellModal.spec.tsx.snap index 3a0e521ec8afb..b2fc2cc2475ca 100644 --- a/apps/meteor/client/components/GenericUpsellModal/__snapshots__/GenericUpsellModal.spec.tsx.snap +++ b/apps/meteor/client/components/GenericUpsellModal/__snapshots__/GenericUpsellModal.spec.tsx.snap @@ -10,7 +10,7 @@ exports[`GenericUpsellModal renders Default without crashing 1`] = ` open="" >
Premium capability
@@ -120,7 +120,7 @@ exports[`GenericUpsellModal renders WithAnnotation without crashing 1`] = ` open="" >
Premium capability
@@ -235,7 +235,7 @@ exports[`GenericUpsellModal renders WithCustomButtons without crashing 1`] = ` open="" >
Premium capability
@@ -345,7 +345,7 @@ exports[`GenericUpsellModal renders WithCustomTagLine without crashing 1`] = ` open="" >
Exclusive
@@ -455,7 +455,7 @@ exports[`GenericUpsellModal renders WithFixedWidthAndHeight without crashing 1`] open="" >
Premium capability
diff --git a/apps/meteor/client/components/Header/Header.tsx b/apps/meteor/client/components/Header/Header.tsx deleted file mode 100644 index 4d8ba472aa65f..0000000000000 --- a/apps/meteor/client/components/Header/Header.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, HeaderV2, Header as HeaderComponent } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const Header = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(Header); diff --git a/apps/meteor/client/components/Header/HeaderAvatar.tsx b/apps/meteor/client/components/Header/HeaderAvatar.tsx deleted file mode 100644 index 917201e7f61a9..0000000000000 --- a/apps/meteor/client/components/Header/HeaderAvatar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Avatar, - HeaderAvatar as HeaderAvatarComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderAvatar = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderAvatar); diff --git a/apps/meteor/client/components/Header/HeaderContent.tsx b/apps/meteor/client/components/Header/HeaderContent.tsx deleted file mode 100644 index 4e81de88388e3..0000000000000 --- a/apps/meteor/client/components/Header/HeaderContent.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Content, - HeaderContent as HeaderContentComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderContent = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderContent); diff --git a/apps/meteor/client/components/Header/HeaderContentRow.tsx b/apps/meteor/client/components/Header/HeaderContentRow.tsx deleted file mode 100644 index 19142aaaba45a..0000000000000 --- a/apps/meteor/client/components/Header/HeaderContentRow.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2ContentRow, - HeaderContentRow as HeaderContentRowComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderContentRow = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderContentRow); diff --git a/apps/meteor/client/components/Header/HeaderDivider.tsx b/apps/meteor/client/components/Header/HeaderDivider.tsx deleted file mode 100644 index 35213e868fba7..0000000000000 --- a/apps/meteor/client/components/Header/HeaderDivider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Divider, - HeaderDivider as HeaderDividerComponent, -} from '@rocket.chat/ui-client'; -import { memo } from 'react'; - -const HeaderDivider = () => ( - - - - - - - - -); - -export default memo(HeaderDivider); diff --git a/apps/meteor/client/components/Header/HeaderIcon.tsx b/apps/meteor/client/components/Header/HeaderIcon.tsx deleted file mode 100644 index eec89379a0909..0000000000000 --- a/apps/meteor/client/components/Header/HeaderIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Icon, - HeaderIcon as HeaderIconComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderIcon = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderIcon); diff --git a/apps/meteor/client/components/Header/HeaderState.tsx b/apps/meteor/client/components/Header/HeaderState.tsx deleted file mode 100644 index aaf2868cdeea3..0000000000000 --- a/apps/meteor/client/components/Header/HeaderState.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2State, - HeaderState as HeaderStateComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderState = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderState); diff --git a/apps/meteor/client/components/Header/HeaderSubtitle.tsx b/apps/meteor/client/components/Header/HeaderSubtitle.tsx deleted file mode 100644 index a847607f08d1c..0000000000000 --- a/apps/meteor/client/components/Header/HeaderSubtitle.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Subtitle, - HeaderSubtitle as HeaderSubtitleComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderSubtitle = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderSubtitle); diff --git a/apps/meteor/client/components/Header/HeaderTag.tsx b/apps/meteor/client/components/Header/HeaderTag.tsx deleted file mode 100644 index a2c3a9397b5bb..0000000000000 --- a/apps/meteor/client/components/Header/HeaderTag.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, HeaderV2Tag, HeaderTag as HeaderTagComponent } from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderTag = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderTag); diff --git a/apps/meteor/client/components/Header/HeaderTagIcon.tsx b/apps/meteor/client/components/Header/HeaderTagIcon.tsx deleted file mode 100644 index bab32c0478b3e..0000000000000 --- a/apps/meteor/client/components/Header/HeaderTagIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2TagIcon, - HeaderTagIcon as HeaderTagIconComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderTagIcon = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderTagIcon); diff --git a/apps/meteor/client/components/Header/HeaderTagSkeleton.tsx b/apps/meteor/client/components/Header/HeaderTagSkeleton.tsx deleted file mode 100644 index 38a94d928fe7f..0000000000000 --- a/apps/meteor/client/components/Header/HeaderTagSkeleton.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2TagSkeleton, - HeaderTagSkeleton as HeaderTagSkeletonComponent, -} from '@rocket.chat/ui-client'; -import { memo } from 'react'; - -const HeaderTagSkeleton = () => ( - - - - - - - - -); - -export default memo(HeaderTagSkeleton); diff --git a/apps/meteor/client/components/Header/HeaderTitle.tsx b/apps/meteor/client/components/Header/HeaderTitle.tsx deleted file mode 100644 index 8490a422682a2..0000000000000 --- a/apps/meteor/client/components/Header/HeaderTitle.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Title, - HeaderTitle as HeaderTitleComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderTitle = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderTitle); diff --git a/apps/meteor/client/components/Header/HeaderTitleButton.tsx b/apps/meteor/client/components/Header/HeaderTitleButton.tsx deleted file mode 100644 index 678851e35a643..0000000000000 --- a/apps/meteor/client/components/Header/HeaderTitleButton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2TitleButton, - HeaderTitleButton as HeaderTitleButtonComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderTitleButton = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderTitleButton); diff --git a/apps/meteor/client/components/Header/HeaderToolbar.tsx b/apps/meteor/client/components/Header/HeaderToolbar.tsx deleted file mode 100644 index b9c66d9b5c6f8..0000000000000 --- a/apps/meteor/client/components/Header/HeaderToolbar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2Toolbar, - HeaderToolbar as HeaderToolbarComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderToolbar = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderToolbar); diff --git a/apps/meteor/client/components/Header/HeaderToolbarAction.tsx b/apps/meteor/client/components/Header/HeaderToolbarAction.tsx deleted file mode 100644 index 1ab382ada1c75..0000000000000 --- a/apps/meteor/client/components/Header/HeaderToolbarAction.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2ToolbarAction, - HeaderToolbarAction as HeaderToolbarActionComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { forwardRef, memo } from 'react'; - -const HeaderToolbarAction = forwardRef>( - function HeaderToolbarAction(props, ref) { - return ( - - - - - - - - - ); - }, -); - -export default memo(HeaderToolbarAction); diff --git a/apps/meteor/client/components/Header/HeaderToolbarActionBadge.tsx b/apps/meteor/client/components/Header/HeaderToolbarActionBadge.tsx deleted file mode 100644 index 4be296169478f..0000000000000 --- a/apps/meteor/client/components/Header/HeaderToolbarActionBadge.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2ToolbarActionBadge, - HeaderToolbarActionBadge as HeaderToolbarActionBadgeComponent, -} from '@rocket.chat/ui-client'; -import type { ComponentProps } from 'react'; -import { memo } from 'react'; - -const HeaderToolbarActionBadge = (props: ComponentProps) => ( - - - - - - - - -); - -export default memo(HeaderToolbarActionBadge); diff --git a/apps/meteor/client/components/Header/HeaderToolbarDivider.tsx b/apps/meteor/client/components/Header/HeaderToolbarDivider.tsx deleted file mode 100644 index 0eaae2feebc00..0000000000000 --- a/apps/meteor/client/components/Header/HeaderToolbarDivider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { - FeaturePreview, - FeaturePreviewOff, - FeaturePreviewOn, - HeaderV2ToolbarDivider, - HeaderToolbarDivider as HeaderToolbarDividerComponent, -} from '@rocket.chat/ui-client'; -import { memo } from 'react'; - -const HeaderToolbarDivider = () => ( - - - - - - - - -); - -export default memo(HeaderToolbarDivider); diff --git a/apps/meteor/client/components/Header/index.ts b/apps/meteor/client/components/Header/index.ts deleted file mode 100644 index be01ea638c980..0000000000000 --- a/apps/meteor/client/components/Header/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Header from './Header'; -import HeaderAvatar from './HeaderAvatar'; -import HeaderContent from './HeaderContent'; -import HeaderContentRow from './HeaderContentRow'; -import HeaderDivider from './HeaderDivider'; -import HeaderIcon from './HeaderIcon'; -import HeaderState from './HeaderState'; -import HeaderSubtitle from './HeaderSubtitle'; -import HeaderTag from './HeaderTag'; -import HeaderTagIcon from './HeaderTagIcon'; -import HeaderTagSkeleton from './HeaderTagSkeleton'; -import HeaderTitle from './HeaderTitle'; -import HeaderTitleButton from './HeaderTitleButton'; -import HeaderToolbar from './HeaderToolbar'; -import HeaderToolbarAction from './HeaderToolbarAction'; -import HeaderToolbarActionBadge from './HeaderToolbarActionBadge'; -import HeaderToolbarDivider from './HeaderToolbarDivider'; - -export { - Header, - HeaderAvatar, - HeaderContent, - HeaderContentRow, - HeaderDivider, - HeaderIcon, - HeaderState, - HeaderSubtitle, - HeaderTag, - HeaderTagIcon, - HeaderTagSkeleton, - HeaderTitle, - HeaderTitleButton, - HeaderToolbar, - HeaderToolbarAction, - HeaderToolbarActionBadge, - HeaderToolbarDivider, -}; diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx index 015cc2b46060a..9da876c2050bc 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx @@ -1,29 +1,24 @@ -import { render, screen } from '@testing-library/react'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; -import RetentionPolicyCallout from './RetentionPolicyCallout'; -import { createRenteionPolicySettingsMock as createMock } from '../../../tests/mocks/client/mockRetentionPolicySettings'; -import { createFakeRoom } from '../../../tests/mocks/data'; +import * as stories from './RetentionPolicyCallout.stories'; -jest.useFakeTimers(); +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); -beforeEach(() => { +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + jest.useFakeTimers(); jest.setSystemTime(new Date(2024, 5, 1, 0, 0, 0)); + + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); }); -describe('RetentionPolicyCallout', () => { - it('Should render callout if settings are valid', () => { - const fakeRoom = createFakeRoom({ t: 'c' }); - render(, { - wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000 }), - }); - expect(screen.getByRole('alert')).toHaveTextContent('a minute June 1, 2024 at 12:30 AM'); - }); +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + // We have to use real timers here because `jest-axe` is breaking otherwise + jest.useRealTimers(); + const { container } = render(); - it('Should not render callout if settings are invalid', () => { - const fakeRoom = createFakeRoom({ t: 'c' }); - render(, { - wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000, advancedPrecisionCron: '* * * 12 *', advancedPrecision: true }), - }); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - }); + const results = await axe(container); + expect(results).toHaveNoViolations(); }); diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.stories.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.stories.tsx new file mode 100644 index 0000000000000..aec43f8d332b3 --- /dev/null +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import RetentionPolicyCallout from './RetentionPolicyCallout'; +import { createRenteionPolicySettingsMock as createMock } from '../../../tests/mocks/client/mockRetentionPolicySettings'; +import { createFakeRoom } from '../../../tests/mocks/data'; + +export default { + component: RetentionPolicyCallout, +} satisfies Meta; + +const fakeRoom = createFakeRoom(); + +const DefaultWrapper = createMock({ appliesToChannels: true, TTLChannels: 60000 }); + +export const Default: StoryFn = () => ( + + + +); + +const InvalidSettingsWrapper = createMock({ + appliesToChannels: true, + TTLChannels: 60000, + advancedPrecisionCron: '* * * 12 * *', + advancedPrecision: true, +}); + +export const InvalidSettings: StoryFn = () => ( + + + +); diff --git a/apps/meteor/client/components/InfoPanel/__snapshots__/RetentionPolicyCallout.spec.tsx.snap b/apps/meteor/client/components/InfoPanel/__snapshots__/RetentionPolicyCallout.spec.tsx.snap new file mode 100644 index 0000000000000..8b1d477b28c52 --- /dev/null +++ b/apps/meteor/client/components/InfoPanel/__snapshots__/RetentionPolicyCallout.spec.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders Default without crashing 1`] = ` + +
+ +
+ +`; + +exports[`renders InvalidSettings without crashing 1`] = ` + +
+ +
+ +`; diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx new file mode 100644 index 0000000000000..b47b32f73497f --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx @@ -0,0 +1,18 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './InvitationBadge.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx new file mode 100644 index 0000000000000..658f206075718 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx @@ -0,0 +1,32 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { Meta } from '@storybook/react'; + +import InvitationBadge from './InvitationBadge'; + +const meta = { + component: InvitationBadge, + parameters: { + layout: 'centered', + }, + decorators: [ + mockAppRoot() + .withTranslations('en', 'core', { + Invited__date__: 'Invited {{date}}', + }) + .buildStoryDecorator(), + ], +} satisfies Meta; + +export default meta; + +export const WithISOStringDate = { + args: { + invitationDate: '2025-01-01T12:00:00Z', + }, +}; + +export const WithDateObject = { + args: { + invitationDate: new Date('2025-01-01T12:00:00Z'), + }, +}; diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx new file mode 100644 index 0000000000000..5c47878208703 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx @@ -0,0 +1,28 @@ +import { Icon } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTimeAgo } from '../../hooks/useTimeAgo'; + +type InvitationBadgeProps = Omit, 'name' | 'color' | 'role'> & { + invitationDate: string | Date; +}; + +const InvitationBadge = ({ invitationDate, ...props }: InvitationBadgeProps) => { + const { t } = useTranslation(); + const timeAgo = useTimeAgo(); + + return ( + + ); +}; + +export default InvitationBadge; diff --git a/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap new file mode 100644 index 0000000000000..da3763aaf4e55 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders WithDateObject without crashing 1`] = ` + +
+ +  + +
+ +`; + +exports[`renders WithISOStringDate without crashing 1`] = ` + +
+ +  + +
+ +`; diff --git a/apps/meteor/client/components/InvitationBadge/index.ts b/apps/meteor/client/components/InvitationBadge/index.ts new file mode 100644 index 0000000000000..78b459e5d05da --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/index.ts @@ -0,0 +1 @@ +export { default } from './InvitationBadge'; diff --git a/apps/meteor/client/components/Page/PageContext.ts b/apps/meteor/client/components/Page/PageContext.ts deleted file mode 100644 index b08b9efd5dbac..0000000000000 --- a/apps/meteor/client/components/Page/PageContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react'; -import { createContext } from 'react'; - -type PageContextValue = [boolean, Dispatch>]; - -const PageContext = createContext([false, (): void => undefined]); - -export default PageContext; diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx deleted file mode 100644 index 15f6f001dec5c..0000000000000 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useDocumentTitle, FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; -import type { ComponentPropsWithoutRef, ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { HeaderToolbar } from '../Header'; -import SidebarToggler from '../SidebarToggler'; - -type PageHeaderProps = { - title: ReactNode; - onClickBack?: () => void; - borderBlockEndColor?: string; -} & Omit, 'title'>; - -const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props }: PageHeaderProps) => { - const { t } = useTranslation(); - const { sidebar, isEmbedded } = useLayout(); - - useDocumentTitle(typeof title === 'string' ? title : undefined); - - return ( - - - - - {sidebar.shouldToggle ? ( - - - - ) : null} - - - {sidebar.shouldToggle && isEmbedded ? ( - - - - ) : null} - - - {onClickBack && } - - {title} - - {children} - - - ); -}; - -export default PageHeaderNoShadow; diff --git a/apps/meteor/client/components/Page/index.ts b/apps/meteor/client/components/Page/index.ts deleted file mode 100644 index 1525eb70186df..0000000000000 --- a/apps/meteor/client/components/Page/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as Page } from './Page'; -export { default as PageContent } from './PageContent'; -export { default as PageFooter } from './PageFooter'; -export { default as PageHeader } from './PageHeader'; -export { default as PageScrollableContent } from './PageScrollableContent'; -export { default as PageScrollableContentWithShadow } from './PageScrollableContentWithShadow'; diff --git a/apps/meteor/client/components/PageSkeleton.tsx b/apps/meteor/client/components/PageSkeleton.tsx index 883b7873f546b..69606f6c3d7ce 100644 --- a/apps/meteor/client/components/PageSkeleton.tsx +++ b/apps/meteor/client/components/PageSkeleton.tsx @@ -1,8 +1,7 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; +import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; -import { Page, PageHeader, PageContent } from './Page'; - const PageSkeleton = (): ReactElement => ( }> diff --git a/apps/meteor/client/components/PlanTag.tsx b/apps/meteor/client/components/PlanTag.tsx index 1687d118f4442..59ad322b0a3cd 100644 --- a/apps/meteor/client/components/PlanTag.tsx +++ b/apps/meteor/client/components/PlanTag.tsx @@ -1,7 +1,7 @@ import { Box, Tag } from '@rocket.chat/fuselage'; +import { useLicense } from '@rocket.chat/ui-client'; import { isTruthy } from '../../lib/isTruthy'; -import { useLicense } from '../hooks/useLicense'; const developmentTag = process.env.NODE_ENV === 'development' ? 'Development' : null; function PlanTag() { diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 66e30775ed799..ee41f907d7ab3 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -71,7 +71,7 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', renderRoomIcon, renderSelected={({ selected: { value, label } }) => ( <> - + {label?.name} diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 87ad4714cc001..401a8879e6093 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -52,7 +52,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet multiple renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( - + {label?.name} @@ -63,7 +63,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet key={value} {...props} label={label.name} - avatar={} + avatar={} /> )} options={options} diff --git a/apps/meteor/client/components/Sidebar/Content.tsx b/apps/meteor/client/components/Sidebar/Content.tsx index 1f87f7ddc0588..b7a3fef60ea49 100644 --- a/apps/meteor/client/components/Sidebar/Content.tsx +++ b/apps/meteor/client/components/Sidebar/Content.tsx @@ -1,8 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; +import { CustomScrollbars } from '@rocket.chat/ui-client'; import type { ComponentPropsWithoutRef } from 'react'; -import { CustomScrollbars } from '../CustomScrollbars'; - type ContentProps = ComponentPropsWithoutRef; const Content = ({ children, ...props }: ContentProps) => ( diff --git a/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx b/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx index 02d22fb868044..033c4065a2e4d 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx @@ -1,10 +1,10 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useEmbeddedLayout } from '@rocket.chat/ui-client'; import { useLayout, useSession } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; import SidebarTogglerButton from './SidebarTogglerButton'; -import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; const SideBarToggler = (): ReactElement => { const { sidebar } = useLayout(); @@ -13,7 +13,13 @@ const SideBarToggler = (): ReactElement => { const toggleSidebar = useEffectEvent(() => sidebar.toggle()); - return ; + return ( + + ); }; export default memo(SideBarToggler); diff --git a/apps/meteor/client/components/SidebarToggler/SidebarTogglerBadge.tsx b/apps/meteor/client/components/SidebarToggler/SidebarTogglerBadge.tsx index 0bcccbe50e555..6de65e701bc89 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarTogglerBadge.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarTogglerBadge.tsx @@ -12,7 +12,7 @@ const SidebarTogglerBadge = ({ children }: SidebarTogglerBadgeProps) => ( position: absolute; z-index: 3; top: -5px; - right: 3px; + right: -5px; `} > {children} diff --git a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx index f147e976000a6..6da63f4786a78 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx @@ -5,16 +5,17 @@ import { useTranslation } from 'react-i18next'; import SidebarTogglerBadge from './SidebarTogglerBadge'; type SideBarTogglerButtonProps = { + pressed?: boolean; badge?: ReactNode; onClick: () => void; }; -const SideBarTogglerButton = ({ badge, onClick }: SideBarTogglerButtonProps) => { +const SideBarTogglerButton = ({ pressed, badge, onClick }: SideBarTogglerButtonProps) => { const { t } = useTranslation(); return ( - + {badge && {badge}} ); diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx deleted file mode 100644 index 6f4762e858b7a..0000000000000 --- a/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useLayout, useSession } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import { memo } from 'react'; - -import SidebarTogglerButton from './SidebarTogglerButton'; -import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; - -const SideBarToggler = (): ReactElement => { - const { sidebar } = useLayout(); - const isLayoutEmbedded = useEmbeddedLayout(); - const unreadMessagesBadge = useSession('unread') as number | string | undefined; - - const toggleSidebar = useEffectEvent(() => sidebar.toggle()); - - return ( - - ); -}; - -export default memo(SideBarToggler); diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx deleted file mode 100644 index 6da63f4786a78..0000000000000 --- a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Box, IconButton } from '@rocket.chat/fuselage'; -import type { ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; - -import SidebarTogglerBadge from './SidebarTogglerBadge'; - -type SideBarTogglerButtonProps = { - pressed?: boolean; - badge?: ReactNode; - onClick: () => void; -}; - -const SideBarTogglerButton = ({ pressed, badge, onClick }: SideBarTogglerButtonProps) => { - const { t } = useTranslation(); - - return ( - - - {badge && {badge}} - - ); -}; - -export default SideBarTogglerButton; diff --git a/apps/meteor/client/components/SidebarTogglerV2/index.ts b/apps/meteor/client/components/SidebarTogglerV2/index.ts deleted file mode 100644 index 4698327ba2938..0000000000000 --- a/apps/meteor/client/components/SidebarTogglerV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as SidebarTogglerV2 } from './SidebarToggler'; diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx index 804a0e5c72e0c..132faf03e2e5c 100644 --- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx @@ -1,16 +1,27 @@ -import { isDirectMessageRoom } from '@rocket.chat/core-typings'; +import { type RoomType, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { AutoComplete, Box, Option, OptionAvatar, OptionContent, Chip } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { RoomAvatar, UserAvatar } from '@rocket.chat/ui-avatar'; +import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useUser, useUserSubscriptions } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, ReactElement } from 'react'; import { memo, useMemo, useState } from 'react'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { Rooms } from '../../stores'; -type UserAndRoomAutoCompleteMultipleProps = Omit, 'filter'> & { limit?: number }; +type UserAndRoomAutoCompleteMultipleProps = Omit, 'filter'> & { + limit?: number; +}; + +type OptionType = { + value: string; + label: { + name: string | undefined; + avatarETag: string | undefined; + type: RoomType; + }; +}[]; const UserAndRoomAutoCompleteMultiple = ({ value, onChange, limit, ...props }: UserAndRoomAutoCompleteMultipleProps) => { const user = useUser(); @@ -35,33 +46,31 @@ const UserAndRoomAutoCompleteMultiple = ({ value, onChange, limit, ...props }: U ), ); - const options = useMemo(() => { - if (!user) { - return []; - } - - return rooms.reduce>((acc, room) => { - if (acc.length === limit) return acc; + const options = useMemo( + () => + rooms.reduce((acc, room) => { + if (acc.length === limit) return acc; - if (isDirectMessageRoom(room) && (room.blocked || room.blocker)) { - return acc; - } + if (isDirectMessageRoom(room) && (room.blocked || room.blocker)) { + return acc; + } - if (roomCoordinator.readOnly(Rooms.state.get(room.rid), user)) return acc; + if (roomCoordinator.readOnly(Rooms.state.get(room.rid), user)) return acc; - return [ - ...acc, - { - value: room.rid, - label: { - name: room.fname || room.name, - avatarETag: room.avatarETag, - type: room.t, + return [ + ...acc, + { + value: room.rid, + label: { + name: room.fname || room.name, + avatarETag: room.avatarETag, + type: room.t, + }, }, - }, - ]; - }, []); - }, [limit, rooms, user]); + ]; + }, []), + [limit, rooms, user], + ); return ( ( + renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( - {label.t === 'd' ? ( - - ) : ( - - )} + {label.name} @@ -86,11 +91,7 @@ const UserAndRoomAutoCompleteMultiple = ({ value, onChange, limit, ...props }: U renderItem={({ value, label, ...props }) => ( diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx index 7c315bdc1aeda..827f88ee309e4 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx @@ -1,55 +1,124 @@ -import { AutoComplete, OptionAvatar, Option, OptionContent, OptionDescription } from '@rocket.chat/fuselage'; +import { MultiSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ComponentProps, ReactElement } from 'react'; -import { memo, useMemo, useState } from 'react'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import type { ReactElement, AllHTMLAttributes } from 'react'; +import { memo, useState, useCallback, useMemo } from 'react'; +import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions'; import UserAvatarChip from './UserAvatarChip'; +import { usersQueryKeys } from '../../lib/queryKeys'; -const query = ( - term = '', -): { - selector: string; -} => ({ selector: JSON.stringify({ term }) }); +type UserAutoCompleteMultipleProps = { + onChange: (value: Array) => void; + value: Array | undefined; + placeholder?: string; + federated?: boolean; + error?: string; +} & Omit, 'is' | 'onChange' | 'value'>; -type UserAutoCompleteMultipleProps = Omit, 'filter'>; +type UserAutoCompleteOptionType = { + name: string; + username: string; + _federated?: boolean; +}; + +type UserAutoCompleteOptions = { + [k: string]: UserAutoCompleteOptionType; +}; -// TODO: useDisplayUsername -const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultipleProps): ReactElement => { +const matrixRegex = new RegExp('@(.*:.*)'); + +const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ...props }: UserAutoCompleteMultipleProps): ReactElement => { const [filter, setFilter] = useState(''); - const debouncedFilter = useDebouncedValue(filter, 1000); - const usersAutoCompleteEndpoint = useEndpoint('GET', '/v1/users.autocomplete'); + const [selectedCache, setSelectedCache] = useState({}); + + const debouncedFilter = useDebouncedValue(filter, 500); + const getUsers = useEndpoint('GET', '/v1/users.autocomplete'); + const { data } = useQuery({ - queryKey: ['usersAutoComplete', debouncedFilter], - queryFn: async () => usersAutoCompleteEndpoint(query(debouncedFilter)), + queryKey: usersQueryKeys.userAutoComplete(debouncedFilter, federated ?? false), + + queryFn: async () => { + const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) }); + const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]); + + // Add extra option if filter text matches `username:server` + // Used to add federated users that do not exist yet + if (federated && matrixRegex.test(debouncedFilter)) { + options.unshift([debouncedFilter, { name: debouncedFilter, username: debouncedFilter, _federated: true }]); + } + + return options; + }, + + placeholderData: keepPreviousData, }); - const options = useMemo(() => data?.items.map((user) => ({ value: user.username, label: user.name })) || [], [data]); + const options = useMemo(() => data || [], [data]); + + const onAddUser = useCallback( + (username: string): void => { + const user = options?.find(([val]) => val === username)?.[1]; + if (!user) { + throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option'); + } + setSelectedCache((selectedCache) => ({ ...selectedCache, [username]: user })); + }, + [setSelectedCache, options], + ); + + const onRemoveUser = useCallback( + (username: string): void => + setSelectedCache((selectedCache) => { + const users = { ...selectedCache }; + delete users[username]; + return users; + }), + [setSelectedCache], + ); + + const handleOnChange = useCallback( + (usernames: string[]) => { + onChange(usernames); + const newAddedUsername = usernames.filter((username) => !value?.includes(username))[0]; + const removedUsername = value?.filter((username) => !usernames.includes(username))[0]; + setFilter(''); + newAddedUsername && onAddUser(newAddedUsername); + removedUsername && onRemoveUser(removedUsername); + }, + [onChange, setFilter, onAddUser, onRemoveUser, value], + ); return ( - ( - - )} - renderItem={({ value, label, ...props }): ReactElement => ( - - )} - options={options} - /> + + void }) => { + const currentCachedOption = selectedCache[username] || {}; + + return ( + + ); + }} + renderOptions={AutocompleteOptions} + options={options.concat(Object.entries(selectedCache)).map(([, item]) => [item.username, item.name || item.username])} + /> + ); }; diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx deleted file mode 100644 index 9c097f5109660..0000000000000 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import type { OptionType } from '@rocket.chat/fuselage'; -import { MultiSelectFiltered } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement, AllHTMLAttributes } from 'react'; -import { memo, useState, useCallback, useMemo } from 'react'; - -import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions'; -import UserAvatarChip from './UserAvatarChip'; - -type UserAutoCompleteMultipleFederatedProps = { - onChange: (value: Array) => void; - value: Array; - placeholder?: string; -} & Omit, 'is' | 'onChange'>; - -type UserAutoCompleteOptionType = { - name: string; - username: string; - _federated?: boolean; -}; - -type UserAutoCompleteOptions = { - [k: string]: UserAutoCompleteOptionType; -}; - -const matrixRegex = new RegExp('@(.*:.*)'); - -const UserAutoCompleteMultipleFederated = ({ - onChange, - value, - placeholder, - ...props -}: UserAutoCompleteMultipleFederatedProps): ReactElement => { - const [filter, setFilter] = useState(''); - const [selectedCache, setSelectedCache] = useState({}); - - const debouncedFilter = useDebouncedValue(filter, 500); - const getUsers = useEndpoint('GET', '/v1/users.autocomplete'); - - const { data } = useQuery({ - queryKey: ['users.autocomplete', debouncedFilter], - - queryFn: async () => { - const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) }); - const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]); - - // Add extra option if filter text matches `username:server` - // Used to add federated users that do not exist yet - if (matrixRegex.test(debouncedFilter)) { - options.unshift([debouncedFilter, { name: debouncedFilter, username: debouncedFilter, _federated: true }]); - } - - return options; - }, - - placeholderData: keepPreviousData, - }); - - const options = useMemo(() => data || [], [data]); - - const onAddUser = useCallback( - (username: string): void => { - const user = options.find(([val]) => val === username)?.[1]; - if (!user) { - throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option'); - } - setSelectedCache((selectedCache) => ({ ...selectedCache, [username]: user })); - }, - [setSelectedCache, options], - ); - - const onRemoveUser = useCallback( - (username: string): void => - setSelectedCache((selectedCache) => { - const users = { ...selectedCache }; - delete users[username]; - return users; - }), - [setSelectedCache], - ); - - const handleOnChange = useCallback( - (usernames: string[]) => { - onChange(usernames); - const newAddedUsername = usernames.filter((username) => !value.includes(username))[0]; - const removedUsername = value.filter((username) => !usernames.includes(username))[0]; - setFilter(''); - newAddedUsername && onAddUser(newAddedUsername); - removedUsername && onRemoveUser(removedUsername); - }, - [onChange, setFilter, onAddUser, onRemoveUser, value], - ); - - return ( - - void }) => { - const currentCachedOption = selectedCache[username] || {}; - - return ( - - ); - }} - renderOptions={AutocompleteOptions} - options={options.concat(Object.entries(selectedCache)).map(([, item]) => [item.username, item.name || item.username])} - data-qa='create-channel-users-autocomplete' - /> - - ); -}; - -export default memo(UserAutoCompleteMultipleFederated); diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx index 318cbdc0365e8..f234cad7ec6da 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx @@ -1,15 +1,17 @@ -import type { IUser } from '@rocket.chat/core-typings'; import { Option, OptionDescription } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import type { ReactElement } from 'react'; + +import type { UserLabel } from './UserAutoCompleteMultipleOptions'; type UserAutoCompleteMultipleOptionProps = { - label: { - _federated?: boolean; - } & Pick; + label: UserLabel; + value: string | number; + selected?: boolean; + focus?: boolean; + role?: string; }; -const UserAutoCompleteMultipleOption = ({ label, ...props }: UserAutoCompleteMultipleOptionProps): ReactElement => { +const UserAutoCompleteMultipleOption = ({ label, ...props }: UserAutoCompleteMultipleOptionProps) => { const { name, username, _federated } = label; return ( @@ -20,13 +22,10 @@ const UserAutoCompleteMultipleOption = ({ label, ...props }: UserAutoCompleteMul icon={_federated ? 'globe' : undefined} key={username} label={ - ( - <> - {name || username} {!_federated && ({username})} - - ) as any + <> + {name || username} {!_federated && ({username})} + } - children={undefined} /> ); }; diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx index 4d8901ff6b661..084bd3a4e388b 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx @@ -9,8 +9,14 @@ import UserAutoCompleteMultipleOption from './UserAutoCompleteMultipleOption'; // The select requires a forwarded ref component in the renderOptions property // but we also need to pass internal state to this renderer, as well as the props that also come from the Select. +export type UserLabel = { + _federated?: boolean; + username: string; + name?: string; +}; + type OptionsContextValue = { - options: OptionType[]; + options: OptionType[]; }; export const OptionsContext = createContext({ diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index cda67cd8e47d8..d8878524fce35 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -1,10 +1,10 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, IconButton } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useEmbeddedLayout } from '@rocket.chat/ui-client'; import type { ReactNode, ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; -import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import MarkdownText from '../MarkdownText'; import * as Status from '../UserStatus'; import UserCardActions from './UserCardActions'; diff --git a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index cf8aa0baad335..65b94ed0d4445 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -1,6 +1,6 @@ +import { ContextualbarDialog } from '@rocket.chat/ui-client'; import type { Meta, StoryFn } from '@storybook/react'; -import { ContextualbarDialog } from '../Contextualbar'; import * as Status from '../UserStatus'; import UserInfo from './UserInfo'; import { UserCardRole } from '../UserCard'; @@ -42,6 +42,19 @@ WithVoiceCallExtension.args = { export const WithABACAttributes = Template.bind({}); WithABACAttributes.args = { - // @ts-expect-error - abacAttributes is not yet implemented in Users properties - abacAttributes: ['Classified', 'Top Secret', 'Confidential'], + abacAttributes: [ + { + key: 'Classified', + values: ['Top Secret', 'Confidential'], + }, + { + key: 'Security_Clearance', + values: ['Top Secret', 'Confidential'], + }, + ], +}; + +export const InvitedUser = Template.bind({}); +InvitedUser.args = { + invitationDate: '2025-01-01T12:00:00Z', }; diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 0cc6a86e36245..59ddb7710a921 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -1,15 +1,8 @@ import type { IUser, Serialized } from '@rocket.chat/core-typings'; import { Box, Margins, Tag } from '@rocket.chat/fuselage'; -import { useUserDisplayName } from '@rocket.chat/ui-client'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ReactNode } from 'react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useTimeAgo } from '../../hooks/useTimeAgo'; -import { useUserCustomFields } from '../../hooks/useUserCustomFields'; -import { ContextualbarScrollableContent } from '../Contextualbar'; import { + useUserDisplayName, + ContextualbarScrollableContent, InfoPanel, InfoPanelActionGroup, InfoPanelAvatar, @@ -18,7 +11,14 @@ import { InfoPanelSection, InfoPanelText, InfoPanelTitle, -} from '../InfoPanel'; +} from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { ReactElement, ReactNode } from 'react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTimeAgo } from '../../hooks/useTimeAgo'; +import { useUserCustomFields } from '../../hooks/useUserCustomFields'; import MarkdownText from '../MarkdownText'; import UTCClock from '../UTCClock'; import { UserCardRoles } from '../UserCard'; @@ -41,6 +41,7 @@ type UserInfoDataProps = Serialized< | 'canViewAllInfo' | 'customFields' | 'freeSwitchExtension' + | 'abacAttributes' > >; @@ -51,6 +52,7 @@ type UserInfoProps = UserInfoDataProps & { actions: ReactElement; roles: ReactElement[]; reason?: string; + invitationDate?: string; }; const UserInfo = ({ @@ -73,8 +75,8 @@ const UserInfo = ({ actions, reason, freeSwitchExtension, - // @ts-expect-error - abacAttributes is not yet implemented in Users properties - abacAttributes = null, + abacAttributes, + invitationDate, ...props }: UserInfoProps): ReactElement => { const { t } = useTranslation(); @@ -189,7 +191,7 @@ const UserInfo = ({ )} - {abacAttributes?.length > 0 && ( + {abacAttributes && abacAttributes.length > 0 && ( {t('ABAC_Attributes')} @@ -207,6 +209,13 @@ const UserInfo = ({ ), )} + {invitationDate && ( + + {t('Invitation_date')} + {timeAgo(invitationDate)} + + )} + {createdAt && ( {t('Created_at')} diff --git a/apps/meteor/client/components/UserInfo/UserInfoABACAttributes.tsx b/apps/meteor/client/components/UserInfo/UserInfoABACAttributes.tsx index b9cf2870d3d64..952e7a9afc693 100644 --- a/apps/meteor/client/components/UserInfo/UserInfoABACAttributes.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfoABACAttributes.tsx @@ -1,20 +1,23 @@ +import type { IAbacAttributeDefinition } from '@rocket.chat/core-typings'; import { Box, Margins } from '@rocket.chat/fuselage'; import UserInfoABACAttribute from './UserInfoABACAttribute'; type UserInfoABACAttributesProps = { - abacAttributes: string[]; + abacAttributes: IAbacAttributeDefinition[]; }; const UserInfoABACAttributes = ({ abacAttributes }: UserInfoABACAttributesProps) => { return ( - {abacAttributes.map((attribute, index) => ( - - - - ))} + {abacAttributes.map((attribute, index) => + attribute.values.map((value) => ( + + + + )), + )} ); diff --git a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap index 3a519a834fa8f..b8990e18c6234 100644 --- a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap +++ b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap @@ -8,250 +8,292 @@ exports[`renders Default without crashing 1`] = ` hidden="" />