diff --git a/.changeset/afraid-poets-sparkle.md b/.changeset/afraid-poets-sparkle.md new file mode 100644 index 000000000000..d9669f73a14e --- /dev/null +++ b/.changeset/afraid-poets-sparkle.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Security tab visibility to allow password changes when 2FA/E2E is disabled. diff --git a/.changeset/breezy-starfishes-attack.md b/.changeset/breezy-starfishes-attack.md new file mode 100644 index 000000000000..56bac6f3cebd --- /dev/null +++ b/.changeset/breezy-starfishes-attack.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes error `audio.pause() is not a function` and makes the continuous new room notification (livechat) respect the volume set in user preferences. diff --git a/.changeset/bump-patch-1714661012622.md b/.changeset/bump-patch-1714661012622.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1714661012622.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1715205144181.md b/.changeset/bump-patch-1715205144181.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1715205144181.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/chilly-glasses-sin.md b/.changeset/chilly-glasses-sin.md new file mode 100644 index 000000000000..3ff7f1ef1c1f --- /dev/null +++ b/.changeset/chilly-glasses-sin.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB diff --git a/.changeset/chilly-walls-knock.md b/.changeset/chilly-walls-knock.md new file mode 100644 index 000000000000..4c183061cbe6 --- /dev/null +++ b/.changeset/chilly-walls-knock.md @@ -0,0 +1,13 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue while creating tokens via the special `users.createToken` API was not respecting the maximum login tokens allowed for a user. + +The following endpoint was deprecated and will be removed on version `8.0.0`: + +- `/api/v1/users.createToken` + +The following Meteor method (realtime API) was deprecated and will be removed on version `8.0.0`: + +- `createToken` diff --git a/.changeset/eighty-pans-joke.md b/.changeset/eighty-pans-joke.md new file mode 100644 index 000000000000..83eabdd8f856 --- /dev/null +++ b/.changeset/eighty-pans-joke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Force logout the clients which are actively online, whenever a user resets E2EE keys. diff --git a/.changeset/eleven-news-stare.md b/.changeset/eleven-news-stare.md new file mode 100644 index 000000000000..8bf62b7aeafa --- /dev/null +++ b/.changeset/eleven-news-stare.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with login via SAML not redirecting to invite link diff --git a/.changeset/eleven-seas-explain.md b/.changeset/eleven-seas-explain.md new file mode 100644 index 000000000000..a41d2fbc625d --- /dev/null +++ b/.changeset/eleven-seas-explain.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue that forces the focus on the last message when interacting by mouse on message list diff --git a/.changeset/fair-peaches-cough.md b/.changeset/fair-peaches-cough.md new file mode 100644 index 000000000000..34f7a319924a --- /dev/null +++ b/.changeset/fair-peaches-cough.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/livechat": patch +--- + +Fixes the livechat client ignoring the `livechat_fileuploads_enabled` setting when uploading files diff --git a/.changeset/fifty-cups-sort.md b/.changeset/fifty-cups-sort.md new file mode 100644 index 000000000000..389391ef8cc9 --- /dev/null +++ b/.changeset/fifty-cups-sort.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Created a new endpoint to get a filtered and paginated list of users. diff --git a/.changeset/fifty-planets-rhyme.md b/.changeset/fifty-planets-rhyme.md new file mode 100644 index 000000000000..3d98a1445aaa --- /dev/null +++ b/.changeset/fifty-planets-rhyme.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Deprecate muteUserInRoom and unmuteUserInRoom meteor methods diff --git a/.changeset/flat-socks-act.md b/.changeset/flat-socks-act.md new file mode 100644 index 000000000000..7188779ea7cf --- /dev/null +++ b/.changeset/flat-socks-act.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed wrong `Business hours` validations between different weeks diff --git a/.changeset/four-eyes-sniff.md b/.changeset/four-eyes-sniff.md new file mode 100644 index 000000000000..06c9dac06d05 --- /dev/null +++ b/.changeset/four-eyes-sniff.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/i18n": minor +--- + +Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings. diff --git a/.changeset/fresh-students-remember.md b/.changeset/fresh-students-remember.md new file mode 100644 index 000000000000..50b422f6b7dd --- /dev/null +++ b/.changeset/fresh-students-remember.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where translations would fallback to english some of the times. diff --git a/.changeset/good-ducks-vanish.md b/.changeset/good-ducks-vanish.md new file mode 100644 index 000000000000..3edfc6baca40 --- /dev/null +++ b/.changeset/good-ducks-vanish.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Introduces sidebar navigability, allowing users to navigate on sidebar channels through keyboard diff --git a/.changeset/good-ghosts-doubt.md b/.changeset/good-ghosts-doubt.md new file mode 100644 index 000000000000..5f4ed8f5a36d --- /dev/null +++ b/.changeset/good-ghosts-doubt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Introduces a resizable Contextualbar allowing users to change the width just by dragging it diff --git a/.changeset/green-ways-tie.md b/.changeset/green-ways-tie.md new file mode 100644 index 000000000000..73a334fd32a0 --- /dev/null +++ b/.changeset/green-ways-tie.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed open expanded view (galery mode) for image attachments sent by livechat widget diff --git a/.changeset/heavy-singers-retire.md b/.changeset/heavy-singers-retire.md new file mode 100644 index 000000000000..60244cd4b5f8 --- /dev/null +++ b/.changeset/heavy-singers-retire.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the last threads list item wasn't displaying properly diff --git a/.changeset/lemon-schools-double.md b/.changeset/lemon-schools-double.md new file mode 100644 index 000000000000..b0f623e8d647 --- /dev/null +++ b/.changeset/lemon-schools-double.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed an issue where mentioning a team would trigger the bot message warning that the team is not a part of the channel diff --git a/.changeset/lovely-trainers-kiss.md b/.changeset/lovely-trainers-kiss.md new file mode 100644 index 000000000000..58d6d5159d91 --- /dev/null +++ b/.changeset/lovely-trainers-kiss.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Convert mute/unmute meteor methods to endpoints diff --git a/.changeset/nervous-elephants-jam.md b/.changeset/nervous-elephants-jam.md new file mode 100644 index 000000000000..cc74cd85842e --- /dev/null +++ b/.changeset/nervous-elephants-jam.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added a new setting to automatically disable users from LDAP that can no longer be found by the background sync diff --git a/.changeset/nice-hounds-enjoy.md b/.changeset/nice-hounds-enjoy.md new file mode 100644 index 000000000000..311a29bee54c --- /dev/null +++ b/.changeset/nice-hounds-enjoy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/eslint-config': minor +--- + +Adds eslint-plugin-jsx-a11y plugin to eslint react config diff --git a/.changeset/nine-houses-reply.md b/.changeset/nine-houses-reply.md new file mode 100644 index 000000000000..29bbe0882a76 --- /dev/null +++ b/.changeset/nine-houses-reply.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/livechat": patch +--- + +Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. diff --git a/.changeset/old-geckos-march.md b/.changeset/old-geckos-march.md new file mode 100644 index 000000000000..188a32d3f515 --- /dev/null +++ b/.changeset/old-geckos-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added the ability to serve .well-known paths directly from Rocket.Chat, if using federation, removing the need for special reverse proxy configuration or another component layer for specific types of reverse proxies / loadbalancers. diff --git a/.changeset/pink-ants-sing.md b/.changeset/pink-ants-sing.md new file mode 100644 index 000000000000..7b4841a11561 --- /dev/null +++ b/.changeset/pink-ants-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. +As UI allowed but changes were not saved, this gave the impression that the function was not working. diff --git a/.changeset/pink-parrots-end.md b/.changeset/pink-parrots-end.md new file mode 100644 index 000000000000..9f1863f6915c --- /dev/null +++ b/.changeset/pink-parrots-end.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Created a new endpoint to resend the welcome email to a given user diff --git a/.changeset/plenty-turkeys-move.md b/.changeset/plenty-turkeys-move.md new file mode 100644 index 000000000000..6452ab772e60 --- /dev/null +++ b/.changeset/plenty-turkeys-move.md @@ -0,0 +1,15 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/apps": patch +"@rocket.chat/core-services": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/fuselage-ui-kit": patch +"@rocket.chat/rest-typings": patch +"@rocket.chat/ddp-streamer": patch +"@rocket.chat/presence": patch +"rocketchat-services": patch +--- + +Fixed an issue with some apps that didn't implement executeViewCloseHandler. This causes opened modals to be open forever on UI (unless Esc was clicked). This is because when the UI attempts to close it, it calls the aforementioned handler, and since it didn't exist, apps engine errored out. + +This returned an empty response to the UI, which ignored the response and continued to show the view. diff --git a/.changeset/popular-fishes-lay.md b/.changeset/popular-fishes-lay.md new file mode 100644 index 000000000000..e709c0e35633 --- /dev/null +++ b/.changeset/popular-fishes-lay.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +--- + +**Fixed settings-related statistics not being updated according to the license.** + +We've identified an issue where certain statistics were not reflecting recent license changes. This resulted in outdated information being reported for workspaces. +This change ensures that all reported statistics are current and consider the workspace license. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..91760ecffbb4 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,118 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "6.8.0-develop", + "rocketchat-services": "1.1.29", + "@rocket.chat/account-service": "0.3.11", + "@rocket.chat/authorization-service": "0.3.11", + "@rocket.chat/ddp-streamer": "0.2.10", + "@rocket.chat/omnichannel-transcript": "0.3.11", + "@rocket.chat/presence-service": "0.3.11", + "@rocket.chat/queue-worker": "0.3.11", + "@rocket.chat/stream-hub-service": "0.3.11", + "@rocket.chat/api-client": "0.1.29", + "@rocket.chat/ddp-client": "0.2.20", + "@rocket.chat/license": "0.1.11", + "@rocket.chat/omnichannel-services": "0.1.11", + "@rocket.chat/pdf-worker": "0.0.35", + "@rocket.chat/presence": "0.1.11", + "@rocket.chat/ui-theming": "0.1.2", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/apps": "0.0.2", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.2", + "@rocket.chat/core-services": "0.3.11", + "@rocket.chat/core-typings": "6.8.0-develop", + "@rocket.chat/cron": "0.0.31", + "@rocket.chat/eslint-config": "0.6.2", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/fuselage-ui-kit": "5.0.0", + "@rocket.chat/gazzodown": "5.0.0", + "@rocket.chat/i18n": "0.2.0", + "@rocket.chat/instance-status": "0.0.35", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.15.0", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/mock-providers": "0.0.5", + "@rocket.chat/model-typings": "0.3.7", + "@rocket.chat/models": "0.0.35", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/password-policies": "0.0.2", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.25", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.2", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "6.8.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.10", + "@rocket.chat/tools": "0.2.1", + "@rocket.chat/ui-avatar": "1.0.0", + "@rocket.chat/ui-client": "5.0.0", + "@rocket.chat/ui-composer": "0.1.0", + "@rocket.chat/ui-contexts": "5.0.0", + "@rocket.chat/ui-kit": "0.33.0", + "@rocket.chat/ui-video-conf": "5.0.0", + "@rocket.chat/uikit-playground": "0.2.20", + "@rocket.chat/web-ui-registration": "5.0.0" + }, + "changesets": [ + "afraid-poets-sparkle", + "breezy-starfishes-attack", + "bump-patch-1714661012622", + "bump-patch-1715205144181", + "chilly-glasses-sin", + "chilly-walls-knock", + "eighty-pans-joke", + "eleven-news-stare", + "eleven-seas-explain", + "fair-peaches-cough", + "fifty-cups-sort", + "fifty-planets-rhyme", + "flat-socks-act", + "four-eyes-sniff", + "fresh-students-remember", + "good-ducks-vanish", + "good-ghosts-doubt", + "green-ways-tie", + "heavy-singers-retire", + "lemon-schools-double", + "lovely-trainers-kiss", + "nervous-elephants-jam", + "nice-hounds-enjoy", + "nine-houses-reply", + "old-geckos-march", + "pink-ants-sing", + "pink-parrots-end", + "plenty-turkeys-move", + "popular-fishes-lay", + "proud-experts-taste", + "shaggy-yaks-train", + "sharp-yaks-turn", + "silent-dodos-doubt", + "slow-cows-dance", + "small-moons-matter", + "smart-squids-begin", + "soft-shrimps-beg", + "strange-comics-camp", + "strange-countries-visit", + "strange-rivers-live", + "strong-bananas-flash", + "sweet-books-trade", + "swift-readers-speak", + "tame-ducks-turn", + "thin-peaches-own", + "thirty-hotels-greet", + "tough-boats-beg", + "twelve-seas-battle", + "two-suns-marry", + "wild-keys-obey", + "yellow-lies-judge", + "young-candles-explode" + ] +} diff --git a/.changeset/proud-experts-taste.md b/.changeset/proud-experts-taste.md new file mode 100644 index 000000000000..c6a358d13b20 --- /dev/null +++ b/.changeset/proud-experts-taste.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a language priority issue. It should now respect the following order: User Preference -> Browser Language -> Server Language diff --git a/.changeset/shaggy-yaks-train.md b/.changeset/shaggy-yaks-train.md new file mode 100644 index 000000000000..1dbc97b48228 --- /dev/null +++ b/.changeset/shaggy-yaks-train.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. diff --git a/.changeset/sharp-yaks-turn.md b/.changeset/sharp-yaks-turn.md new file mode 100644 index 000000000000..7c05bf5a3b0e --- /dev/null +++ b/.changeset/sharp-yaks-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed supported versions not being updated in airgapped environments diff --git a/.changeset/silent-dodos-doubt.md b/.changeset/silent-dodos-doubt.md new file mode 100644 index 000000000000..53859f83cc7e --- /dev/null +++ b/.changeset/silent-dodos-doubt.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Added a new notification provider in light of the old FCM API deprecation, now you can choose to use the new provider or the old via the `Push_UseLegacy` setting diff --git a/.changeset/slow-cows-dance.md b/.changeset/slow-cows-dance.md new file mode 100644 index 000000000000..67097c860cf6 --- /dev/null +++ b/.changeset/slow-cows-dance.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels. +This change is to keep the semantics and conventions of the endpoints diff --git a/.changeset/small-moons-matter.md b/.changeset/small-moons-matter.md new file mode 100644 index 000000000000..af18b14ac426 --- /dev/null +++ b/.changeset/small-moons-matter.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed custom OAuth roles not synced on the first login (on user creation) diff --git a/.changeset/smart-squids-begin.md b/.changeset/smart-squids-begin.md new file mode 100644 index 000000000000..48f3f460ea7e --- /dev/null +++ b/.changeset/smart-squids-begin.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing space between name and user name on system messages diff --git a/.changeset/soft-shrimps-beg.md b/.changeset/soft-shrimps-beg.md new file mode 100644 index 000000000000..74bd810a93aa --- /dev/null +++ b/.changeset/soft-shrimps-beg.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +This PR have made enhancements to the select and multiselect inputs related to Omnichannel Departments, now the options properly display the complete department names, ensuring clarity for users and added text wrapping for long department names, enhancing readability and UX. diff --git a/.changeset/strange-comics-camp.md b/.changeset/strange-comics-camp.md new file mode 100644 index 000000000000..667ba409a7f3 --- /dev/null +++ b/.changeset/strange-comics-camp.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where an endpoint was called before checking configuration that enables automatic translation when launching the application diff --git a/.changeset/strange-countries-visit.md b/.changeset/strange-countries-visit.md new file mode 100644 index 000000000000..211dbb2f5a72 --- /dev/null +++ b/.changeset/strange-countries-visit.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +--- + +Fixed a problem that caused OTR Session messages' to not being transmitted from one peer to another when running Rocket.Chat as microservices. This was caused by a legacy streamer that tried to use the websocket directly, which works on monolith but doesn't on microservices, cause these events are routed through DDP Streamer service. diff --git a/.changeset/strange-rivers-live.md b/.changeset/strange-rivers-live.md new file mode 100644 index 000000000000..b1ebd05c284d --- /dev/null +++ b/.changeset/strange-rivers-live.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. +This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. diff --git a/.changeset/strong-bananas-flash.md b/.changeset/strong-bananas-flash.md new file mode 100644 index 000000000000..d41697836d11 --- /dev/null +++ b/.changeset/strong-bananas-flash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button diff --git a/.changeset/sweet-books-trade.md b/.changeset/sweet-books-trade.md new file mode 100644 index 000000000000..be828d662f32 --- /dev/null +++ b/.changeset/sweet-books-trade.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed search room not showing the new name room name changes diff --git a/.changeset/swift-readers-speak.md b/.changeset/swift-readers-speak.md new file mode 100644 index 000000000000..25a109d492d6 --- /dev/null +++ b/.changeset/swift-readers-speak.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Added "Enable Users" option under "Sync User Active State" LDAP setting to allow only re-enabling users found on LDAP background sync diff --git a/.changeset/tame-ducks-turn.md b/.changeset/tame-ducks-turn.md new file mode 100644 index 000000000000..0ad730b9b310 --- /dev/null +++ b/.changeset/tame-ducks-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a problem that caused `afterCreateUser` callback to be called without new user's roles inside. This caused Omnichannel Business Hour manager to ignore these users from assigning open business hours until the manager restarted or the business hour restarted. diff --git a/.changeset/thin-peaches-own.md b/.changeset/thin-peaches-own.md new file mode 100644 index 000000000000..b002e2f774c2 --- /dev/null +++ b/.changeset/thin-peaches-own.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where message reactions are vertically misaligned when zooming out diff --git a/.changeset/thirty-hotels-greet.md b/.changeset/thirty-hotels-greet.md new file mode 100644 index 000000000000..ac226b11b6b6 --- /dev/null +++ b/.changeset/thirty-hotels-greet.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Redesign Save E2EE password modal diff --git a/.changeset/tough-boats-beg.md b/.changeset/tough-boats-beg.md new file mode 100644 index 000000000000..bc77048ffbec --- /dev/null +++ b/.changeset/tough-boats-beg.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Support Message Custom Fields on upload API via field `customField` and JSON value diff --git a/.changeset/twelve-seas-battle.md b/.changeset/twelve-seas-battle.md new file mode 100644 index 000000000000..a527a93f6212 --- /dev/null +++ b/.changeset/twelve-seas-battle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where old exports would get overwritten by new ones if generated on the same day, when using external storage services (such as Amazon S3) diff --git a/.changeset/two-suns-marry.md b/.changeset/two-suns-marry.md new file mode 100644 index 000000000000..3eae6383a62f --- /dev/null +++ b/.changeset/two-suns-marry.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +feat: `ConnectionStatusBar` redesign diff --git a/.changeset/wild-keys-obey.md b/.changeset/wild-keys-obey.md new file mode 100644 index 000000000000..9de92ee5671b --- /dev/null +++ b/.changeset/wild-keys-obey.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes issue causing a desync in different browser windows when a chat is closed and started again diff --git a/.changeset/yellow-lies-judge.md b/.changeset/yellow-lies-judge.md new file mode 100644 index 000000000000..15bc8e0819bd --- /dev/null +++ b/.changeset/yellow-lies-judge.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed Engagement Dashboard and Device Management admin pages loading indefinitely diff --git a/.changeset/young-candles-explode.md b/.changeset/young-candles-explode.md new file mode 100644 index 000000000000..91ff2458a403 --- /dev/null +++ b/.changeset/young-candles-explode.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-contexts': minor +'@rocket.chat/meteor': minor +--- + +Fixed an issue affecting the update modal/contextual bar by apps when it comes to error handling and regular surface update diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 284a0985b78e..364957ecdf01 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -13,6 +13,10 @@ inputs: required: false description: 'Platform' type: string + build-containers: + required: false + description: 'Containers to build along with Rocket.Chat' + type: string runs: using: composite @@ -54,11 +58,7 @@ runs: - name: Build Docker images shell: bash run: | - args=(rocketchat) - - if [[ '${{ inputs.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + args=(rocketchat ${{ inputs.build-containers }}) docker compose -f docker-compose-ci.yml build "${args[@]}" @@ -66,10 +66,6 @@ runs: if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') shell: bash run: | - args=(rocketchat) - - if [[ '${{ inputs.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + args=(rocketchat ${{ inputs.build-containers }}) docker compose -f docker-compose-ci.yml push "${args[@]}" diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 1d953b9eb01f..a7a6f3f367e2 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -83,9 +83,17 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }} (${{ matrix.shard }}/${{ inputs.total-shard }}) + name: MongoDB ${{ matrix.mongodb-version }} (${{ matrix.shard }}/${{ inputs.total-shard }})${{ matrix.mongodb-version == '6.0' && ' - Alpine' || '' }} steps: + - name: Login to GitHub Container Registry + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + - name: Launch MongoDB uses: supercharge/mongodb-github-action@v1.10.0 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a78ca940d03..51e034505a85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,6 @@ jobs: RC_DOCKER_TAG: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-docker-tag-alpine || needs.release-versions.outputs.rc-docker-tag }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} - SERVICES_PUBLISH: 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service' strategy: fail-fast: false @@ -237,6 +236,7 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} + build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} build-gh-docker: name: 🚢 Build Docker Images for Production @@ -248,7 +248,6 @@ jobs: RC_DOCKER_TAG: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-docker-tag-alpine || needs.release-versions.outputs.rc-docker-tag }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} - SERVICES_PUBLISH: 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service' strategy: fail-fast: false @@ -264,6 +263,7 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} + build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' diff --git a/apps/meteor/.meteorMocks/index.ts b/apps/meteor/.meteorMocks/index.ts new file mode 100644 index 000000000000..e70ffa7f7c46 --- /dev/null +++ b/apps/meteor/.meteorMocks/index.ts @@ -0,0 +1,5 @@ +import sinon from 'sinon'; + +export const Meteor = { + loginWithSamlToken: sinon.stub(), +}; diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 54bed06ba34c..e26a8591c435 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,11 +1,216 @@ # @rocket.chat/meteor -## 6.7.2 +## 6.8.0-rc.2 ### Patch Changes - Bump @rocket.chat/meteor version. +- ([#32348](https://github.com/RocketChat/Rocket.Chat/pull/32348)) Fixed an issue where translations would fallback to english some of the times. + +- ([#32374](https://github.com/RocketChat/Rocket.Chat/pull/32374)) Fixed an issue with some apps that didn't implement executeViewCloseHandler. This causes opened modals to be open forever on UI (unless Esc was clicked). This is because when the UI attempts to close it, it calls the aforementioned handler, and since it didn't exist, apps engine errored out. + + This returned an empty response to the UI, which ignored the response and continued to show the view. + +-
Updated dependencies [b94ca7c30b]: + + - @rocket.chat/apps@0.0.5-rc.2 + - @rocket.chat/core-services@0.3.14-rc.2 + - @rocket.chat/core-typings@6.8.0-rc.2 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.2 + - @rocket.chat/rest-typings@6.8.0-rc.2 + - @rocket.chat/presence@0.1.14-rc.2 + - @rocket.chat/omnichannel-services@0.1.14-rc.2 + - @rocket.chat/api-client@0.1.32-rc.2 + - @rocket.chat/license@0.1.14-rc.2 + - @rocket.chat/pdf-worker@0.0.38-rc.2 + - @rocket.chat/cron@0.0.34-rc.2 + - @rocket.chat/gazzodown@6.0.0-rc.2 + - @rocket.chat/model-typings@0.4.0-rc.2 + - @rocket.chat/ui-contexts@6.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.38-rc.2 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.2 + - @rocket.chat/ui-client@6.0.0-rc.2 + - @rocket.chat/ui-video-conf@6.0.0-rc.2 + - @rocket.chat/web-ui-registration@6.0.0-rc.2 + - @rocket.chat/instance-status@0.0.38-rc.2 +
+ +## 6.8.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.8.0-rc.1 + - @rocket.chat/rest-typings@6.8.0-rc.1 + - @rocket.chat/api-client@0.1.31-rc.1 + - @rocket.chat/license@0.1.13-rc.1 + - @rocket.chat/omnichannel-services@0.1.13-rc.1 + - @rocket.chat/pdf-worker@0.0.37-rc.1 + - @rocket.chat/presence@0.1.13-rc.1 + - @rocket.chat/apps@0.0.4-rc.1 + - @rocket.chat/core-services@0.3.13-rc.1 + - @rocket.chat/cron@0.0.33-rc.1 + - @rocket.chat/gazzodown@6.0.0-rc.1 + - @rocket.chat/model-typings@0.4.0-rc.1 + - @rocket.chat/ui-contexts@6.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.1 + - @rocket.chat/models@0.0.37-rc.1 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.1 + - @rocket.chat/ui-client@6.0.0-rc.1 + - @rocket.chat/ui-video-conf@6.0.0-rc.1 + - @rocket.chat/web-ui-registration@6.0.0-rc.1 + - @rocket.chat/instance-status@0.0.37-rc.1 +
+ +## 6.8.0-rc.0 + +### Minor Changes + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to get a filtered and paginated list of users. + +- ([#32224](https://github.com/RocketChat/Rocket.Chat/pull/32224)) Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings. + +- ([#32115](https://github.com/RocketChat/Rocket.Chat/pull/32115)) Introduces sidebar navigability, allowing users to navigate on sidebar channels through keyboard + +- ([#29461](https://github.com/RocketChat/Rocket.Chat/pull/29461)) Introduces a resizable Contextualbar allowing users to change the width just by dragging it + +- ([#31840](https://github.com/RocketChat/Rocket.Chat/pull/31840)) Encrypt file descriptions in E2EE rooms + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Convert mute/unmute meteor methods to endpoints + +- ([#32084](https://github.com/RocketChat/Rocket.Chat/pull/32084)) Added a new setting to automatically disable users from LDAP that can no longer be found by the background sync + +- ([#31965](https://github.com/RocketChat/Rocket.Chat/pull/31965)) Added the ability to serve .well-known paths directly from Rocket.Chat, if using federation, removing the need for special reverse proxy configuration or another component layer for specific types of reverse proxies / loadbalancers. + +- ([#31898](https://github.com/RocketChat/Rocket.Chat/pull/31898)) Created a new endpoint to resend the welcome email to a given user + +- ([#32208](https://github.com/RocketChat/Rocket.Chat/pull/32208)) Added a new notification provider in light of the old FCM API deprecation, now you can choose to use the new provider or the old via the `Push_UseLegacy` setting + +- ([#31976](https://github.com/RocketChat/Rocket.Chat/pull/31976)) Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. + This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. +- ([#32173](https://github.com/RocketChat/Rocket.Chat/pull/32173)) Added "Enable Users" option under "Sync User Active State" LDAP setting to allow only re-enabling users found on LDAP background sync + +- ([#31865](https://github.com/RocketChat/Rocket.Chat/pull/31865)) Redesign Save E2EE password modal + +- ([#32272](https://github.com/RocketChat/Rocket.Chat/pull/32272)) Support Message Custom Fields on upload API via field `customField` and JSON value + +- ([#32055](https://github.com/RocketChat/Rocket.Chat/pull/32055)) feat: `ConnectionStatusBar` redesign + +- ([#32073](https://github.com/RocketChat/Rocket.Chat/pull/32073)) Fixed an issue affecting the update modal/contextual bar by apps when it comes to error handling and regular surface update + +### Patch Changes + +- ([#31996](https://github.com/RocketChat/Rocket.Chat/pull/31996)) Fixed Security tab visibility to allow password changes when 2FA/E2E is disabled. + +- ([#32210](https://github.com/RocketChat/Rocket.Chat/pull/32210)) Fixes error `audio.pause() is not a function` and makes the continuous new room notification (livechat) respect the volume set in user preferences. + +- ([#32186](https://github.com/RocketChat/Rocket.Chat/pull/32186)) Increased the timeout between calls for the three remaining Omnichannel Agenda Jobs. This should make them happen less often and reduce the load on MongoDB + +- ([#32216](https://github.com/RocketChat/Rocket.Chat/pull/32216)) Fixed an issue while creating tokens via the special `users.createToken` API was not respecting the maximum login tokens allowed for a user. + + The following endpoint was deprecated and will be removed on version `8.0.0`: + + - `/api/v1/users.createToken` + + The following Meteor method (realtime API) was deprecated and will be removed on version `8.0.0`: + + - `createToken` + +- ([#31958](https://github.com/RocketChat/Rocket.Chat/pull/31958)) Force logout the clients which are actively online, whenever a user resets E2EE keys. + +- ([#31989](https://github.com/RocketChat/Rocket.Chat/pull/31989)) Fixed issue with login via SAML not redirecting to invite link + +- ([#32187](https://github.com/RocketChat/Rocket.Chat/pull/32187)) Fixes an issue that forces the focus on the last message when interacting by mouse on message list + +- ([#31765](https://github.com/RocketChat/Rocket.Chat/pull/31765)) Fixes the livechat client ignoring the `livechat_fileuploads_enabled` setting when uploading files + +- ([#31811](https://github.com/RocketChat/Rocket.Chat/pull/31811)) Deprecate muteUserInRoom and unmuteUserInRoom meteor methods + +- ([#32287](https://github.com/RocketChat/Rocket.Chat/pull/32287)) Fixed wrong `Business hours` validations between different weeks + +- ([#31990](https://github.com/RocketChat/Rocket.Chat/pull/31990)) Fixed open expanded view (galery mode) for image attachments sent by livechat widget + +- ([#32248](https://github.com/RocketChat/Rocket.Chat/pull/32248)) Fixes an issue where the last threads list item wasn't displaying properly + +- ([#32112](https://github.com/RocketChat/Rocket.Chat/pull/32112)) fixed an issue where mentioning a team would trigger the bot message warning that the team is not a part of the channel + +- ([#32069](https://github.com/RocketChat/Rocket.Chat/pull/32069)) Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. + +- ([#32063](https://github.com/RocketChat/Rocket.Chat/pull/32063)) Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. + As UI allowed but changes were not saved, this gave the impression that the function was not working. +- ([#32237](https://github.com/RocketChat/Rocket.Chat/pull/32237)) **Fixed settings-related statistics not being updated according to the license.** + + We've identified an issue where certain statistics were not reflecting recent license changes. This resulted in outdated information being reported for workspaces. + This change ensures that all reported statistics are current and consider the workspace license. + +- ([#32170](https://github.com/RocketChat/Rocket.Chat/pull/32170)) Fixed a language priority issue. It should now respect the following order: User Preference -> Browser Language -> Server Language + +- ([#32202](https://github.com/RocketChat/Rocket.Chat/pull/32202)) Fixed an issue where Rocket.Chat would ask admins to confirm fingerprint change (new workspace vs configuration update), even when `AUTO_ACCEPT_FINGERPRINT` environment variable set to `"true"`. + +- ([#32220](https://github.com/RocketChat/Rocket.Chat/pull/32220)) Fixed supported versions not being updated in airgapped environments + +- ([#32141](https://github.com/RocketChat/Rocket.Chat/pull/32141)) Deprecate `channels.images` in favor of `rooms.images`. `Rooms` endpoints are more broad and should interact with all types of rooms. `Channels` on the other hand are specific to public channels. + This change is to keep the semantics and conventions of the endpoints +- ([#32157](https://github.com/RocketChat/Rocket.Chat/pull/32157)) Fixed custom OAuth roles not synced on the first login (on user creation) + +- ([#32136](https://github.com/RocketChat/Rocket.Chat/pull/32136)) Fixes the missing space between name and user name on system messages + +- ([#32007](https://github.com/RocketChat/Rocket.Chat/pull/32007)) This PR have made enhancements to the select and multiselect inputs related to Omnichannel Departments, now the options properly display the complete department names, ensuring clarity for users and added text wrapping for long department names, enhancing readability and UX. + +- ([#32221](https://github.com/RocketChat/Rocket.Chat/pull/32221)) Fixed an issue where an endpoint was called before checking configuration that enables automatic translation when launching the application + +- ([#32230](https://github.com/RocketChat/Rocket.Chat/pull/32230)) Fixed a problem that caused OTR Session messages' to not being transmitted from one peer to another when running Rocket.Chat as microservices. This was caused by a legacy streamer that tried to use the websocket directly, which works on monolith but doesn't on microservices, cause these events are routed through DDP Streamer service. + +- ([#32021](https://github.com/RocketChat/Rocket.Chat/pull/32021)) Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button + +- ([#32123](https://github.com/RocketChat/Rocket.Chat/pull/32123)) fixed search room not showing the new name room name changes + +- ([#30309](https://github.com/RocketChat/Rocket.Chat/pull/30309)) Fixed a problem that caused `afterCreateUser` callback to be called without new user's roles inside. This caused Omnichannel Business Hour manager to ignore these users from assigning open business hours until the manager restarted or the business hour restarted. + +- ([#32172](https://github.com/RocketChat/Rocket.Chat/pull/32172)) Fixes an issue where message reactions are vertically misaligned when zooming out + +- ([#32062](https://github.com/RocketChat/Rocket.Chat/pull/32062)) Fixed an issue where old exports would get overwritten by new ones if generated on the same day, when using external storage services (such as Amazon S3) + +-
Updated dependencies [845fd64f45, c47a8e3514, 9a6a7d0a40, da45cb6998, 845fd64f45, 9902554388, 8b0986d15a, 4aba7c8a26, c4e58afd8b, c9a92e6ea2, c0d54d742a]: + + - @rocket.chat/rest-typings@6.8.0-rc.0 + - @rocket.chat/core-typings@6.8.0-rc.0 + - @rocket.chat/i18n@0.3.0-rc.0 + - @rocket.chat/model-typings@0.4.0-rc.0 + - @rocket.chat/core-services@0.3.12-rc.0 + - @rocket.chat/ui-contexts@6.0.0-rc.0 + - @rocket.chat/api-client@0.1.30-rc.0 + - @rocket.chat/omnichannel-services@0.1.12-rc.0 + - @rocket.chat/presence@0.1.12-rc.0 + - @rocket.chat/license@0.1.12-rc.0 + - @rocket.chat/pdf-worker@0.0.36-rc.0 + - @rocket.chat/apps@0.0.3-rc.0 + - @rocket.chat/cron@0.0.32-rc.0 + - @rocket.chat/gazzodown@6.0.0-rc.0 + - @rocket.chat/web-ui-registration@6.0.0-rc.0 + - @rocket.chat/models@0.0.36-rc.0 + - @rocket.chat/base64@1.0.13 + - @rocket.chat/fuselage-ui-kit@6.0.0-rc.0 + - @rocket.chat/instance-status@0.0.36-rc.0 + - @rocket.chat/message-parser@0.31.29 + - @rocket.chat/random@1.2.2 + - @rocket.chat/sha256@1.0.10 + - @rocket.chat/ui-composer@0.1.0 + - @rocket.chat/ui-kit@0.33.0 + - @rocket.chat/ui-video-conf@6.0.0-rc.0 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-avatar@2.0.0-rc.0 + - @rocket.chat/ui-client@6.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 + - Bump @rocket.chat/meteor version. - ([#32315](https://github.com/RocketChat/Rocket.Chat/pull/32315) by [@dionisio-bot](https://github.com/dionisio-bot)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely @@ -36,6 +241,14 @@ - @rocket.chat/instance-status@0.0.37
+## 6.7.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32284](https://github.com/RocketChat/Rocket.Chat/pull/32284)) fixed Engagement Dashboard and Device Management admin pages loading indefinitely + ## 6.7.1 ### Patch Changes diff --git a/apps/meteor/app/api/server/api.helpers.ts b/apps/meteor/app/api/server/api.helpers.ts index bbc429e0bbc5..365e50701685 100644 --- a/apps/meteor/app/api/server/api.helpers.ts +++ b/apps/meteor/app/api/server/api.helpers.ts @@ -1,6 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { hasAllPermissionAsync, hasAtLeastOnePermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { apiDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | '*'; export type PermissionsPayload = { @@ -101,3 +102,8 @@ export function checkPermissions(options: { permissionsRequired?: PermissionsReq // If reached here, options.permissionsRequired contained an invalid payload return false; } + +export function parseDeprecation(methodThis: any, { alternatives, version }: { version: string; alternatives?: string[] }): void { + const infoMessage = alternatives?.length ? ` Please use the alternative(s): ${alternatives.join(',')}` : ''; + apiDeprecationLogger.endpoint(methodThis.request.route, version, methodThis.response, infoMessage); +} diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 08e1ef17e348..87153440cd28 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -17,12 +17,11 @@ import { isObject } from '../../../lib/utils/isObject'; import { getRestPayload } from '../../../server/lib/logger/logPayloads'; import { checkCodeForUser } from '../../2fa/server/code'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { apiDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; import { metrics } from '../../metrics/server'; import { settings } from '../../settings/server'; import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; import type { PermissionsPayload } from './api.helpers'; -import { checkPermissionsForInvocation, checkPermissions } from './api.helpers'; +import { checkPermissionsForInvocation, checkPermissions, parseDeprecation } from './api.helpers'; import type { FailureResult, InternalError, @@ -588,8 +587,8 @@ export class APIClass extends Restivus { const connection = { ...generateConnection(this.requestIp, this.request.headers), token: this.token }; try { - if (options.deprecationVersion) { - apiDeprecationLogger.endpoint(this.request.route, options.deprecationVersion, this.response); + if (options.deprecation) { + parseDeprecation(this, options.deprecation); } await api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId); diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index d2fa248530ff..b9825a4f9612 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -95,7 +95,10 @@ export type Options = ( ) & { validateParams?: ValidateFunction | { [key in Method]?: ValidateFunction }; authOrAnonRequired?: true; - deprecationVersion?: string; + deprecation?: { + version: string; + alternatives?: string[]; + }; }; export type PartialThis = { diff --git a/apps/meteor/app/api/server/helpers/getInstanceList.ts b/apps/meteor/app/api/server/helpers/getInstanceList.ts new file mode 100644 index 000000000000..d63cb1ef750e --- /dev/null +++ b/apps/meteor/app/api/server/helpers/getInstanceList.ts @@ -0,0 +1,4 @@ +import { makeFunction } from '@rocket.chat/patch-injection'; +import type { BrokerNode } from 'moleculer'; + +export const getInstanceList = makeFunction(async (): Promise => []); diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 990fda0a0209..f80d662771df 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -2,7 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Mongo } from 'meteor/mongo'; -import type { Filter } from 'mongodb'; +import type { Filter, RootFilterOperators } from 'mongodb'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; @@ -119,3 +119,98 @@ export function getNonEmptyQuery(query: Mongo.Query | undefi return { ...defaultQuery, ...query }; } + +type FindPaginatedUsersByStatusProps = { + uid: string; + offset: number; + count: number; + sort: Record; + status: 'active' | 'deactivated'; + roles: string[] | null; + searchTerm: string; + hasLoggedIn: boolean; + type: string; +}; + +export async function findPaginatedUsersByStatus({ + uid, + offset, + count, + sort, + status, + roles, + searchTerm, + hasLoggedIn, + type, +}: FindPaginatedUsersByStatusProps) { + const projection = { + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + active: 1, + avatarETag: 1, + lastLogin: 1, + type: 1, + reason: 1, + }; + + const actualSort: Record = sort || { username: 1 }; + if (sort?.status) { + actualSort.active = sort.status; + } + if (sort?.name) { + actualSort.nameInsensitive = sort.name; + } + const match: Filter> = {}; + switch (status) { + case 'active': + match.active = true; + break; + case 'deactivated': + match.active = false; + break; + } + + if (hasLoggedIn !== undefined) { + match.lastLogin = { $exists: hasLoggedIn }; + } + + if (type) { + match.type = type; + } + + const canSeeAllUserInfo = await hasPermissionAsync(uid, 'view-full-other-user-info'); + + match.$or = [ + ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm || ''), $options: 'i' } }] : []), + { + username: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + { + name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + ]; + if (roles?.length && !roles.includes('all')) { + match.roles = { $in: roles }; + } + const { cursor, totalCount } = await Users.findPaginated( + { + ...match, + }, + { + sort: actualSort, + skip: offset, + limit: count, + projection, + }, + ); + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); + return { + users, + count: users.length, + offset, + total, + }; +} diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 1c84926edb63..931cf4be2019 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -18,7 +18,7 @@ import { isChannelsConvertToTeamProps, isChannelsSetReadOnlyProps, isChannelsDeleteProps, - isChannelsImagesProps, + isRoomsImagesProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -806,7 +806,14 @@ API.v1.addRoute( API.v1.addRoute( 'channels.images', - { authRequired: true, validateParams: isChannelsImagesProps }, + { + authRequired: true, + validateParams: isRoomsImagesProps, + deprecation: { + version: '7.0.0', + alternatives: ['rooms.images'], + }, + }, { async get() { const room = await Rooms.findOneById>(this.queryParams.roomId, { diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 98fc278594ae..c482e3bb784d 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -312,6 +312,7 @@ API.v1.addRoute( roomId: String, msgId: String, text: String, // Using text to be consistant with chat.postMessage + customFields: Match.Maybe(Object), previewUrls: Match.Maybe([String]), }), ); @@ -328,7 +329,16 @@ API.v1.addRoute( } // Permission checks are already done in the updateMessage method, so no need to duplicate them - await executeUpdateMessage(this.userId, { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }, this.bodyParams.previewUrls); + await executeUpdateMessage( + this.userId, + { + _id: msg._id, + msg: this.bodyParams.text, + rid: msg.rid, + customFields: this.bodyParams.customFields as Record | undefined, + }, + this.bodyParams.previewUrls, + ); const updatedMessage = await Messages.findOneById(msg._id); const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId); diff --git a/apps/meteor/app/api/server/v1/instances.ts b/apps/meteor/app/api/server/v1/instances.ts index 7b3482e0a7b8..e5404ab3e53c 100644 --- a/apps/meteor/app/api/server/v1/instances.ts +++ b/apps/meteor/app/api/server/v1/instances.ts @@ -1,16 +1,16 @@ import { InstanceStatus } from '@rocket.chat/models'; -import { Instance as InstanceService } from '../../../../ee/server/sdk'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { API } from '../api'; +import { getInstanceList } from '../helpers/getInstanceList'; -const getMatrixInstances = (() => { +const getConnections = (() => { if (isRunningMs()) { return () => []; } - return () => InstanceService.getInstances(); + return () => getInstanceList(); })(); API.v1.addRoute( @@ -24,7 +24,7 @@ API.v1.addRoute( const instanceRecords = await InstanceStatus.find().toArray(); - const connections = await getMatrixInstances(); + const connections = await getConnections(); const result = instanceRecords.map((instanceRecord) => { const connection = connections.find((c) => c.id === instanceRecord._id); diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 7b6c964a50bb..bdf6fa2dd1c6 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -24,9 +24,9 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { getLogs } from '../../../../server/stream/stdout'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { passwordPolicy } from '../../../lib/server'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; +import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; import { API } from '../api'; import { getLoggedInUser } from '../helpers/getLoggedInUser'; @@ -409,10 +409,13 @@ API.v1.addRoute( { authRequired: false, validateParams: validateParamsPwGetPolicyRest, + deprecation: { + version: '7.0.0', + alternatives: ['pw.getPolicy'], + }, }, { async get() { - apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, ' Use pw.getPolicy instead.'); check( this.queryParams, Match.ObjectIncluding({ @@ -634,9 +637,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); - const isSMTPConfigured = Boolean(settings.get('SMTP_Host')) || isMailURLSet; - return API.v1.success({ isSMTPConfigured }); + return API.v1.success({ isSMTPConfigured: isSMTPConfigured() }); }, }, ); diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index ae08dae04938..9576a79f6678 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -1,13 +1,15 @@ import { Media } from '@rocket.chat/core-services'; -import type { IRoom } from '@rocket.chat/core-typings'; -import { Messages, Rooms, Users } from '@rocket.chat/models'; +import type { IRoom, IUpload } from '@rocket.chat/core-typings'; +import { Messages, Rooms, Users, Uploads } from '@rocket.chat/models'; import type { Notifications } from '@rocket.chat/rest-typings'; -import { isGETRoomsNameExists } from '@rocket.chat/rest-typings'; +import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/methods/eraseRoom'; +import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; +import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; @@ -19,6 +21,7 @@ import { settings } from '../../../settings/server'; import { API } from '../api'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getPaginationItems } from '../helpers/getPaginationItems'; +import { getUserFromParams } from '../helpers/getUserFromParams'; import { getUploadFormData } from '../lib/getUploadFormData'; import { findAdminRoom, @@ -322,7 +325,7 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { prid, pmid, reply, t_name, users, encrypted } = this.bodyParams; + const { prid, pmid, reply, t_name, users, encrypted, topic } = this.bodyParams; if (!prid) { return API.v1.failure('Body parameter "prid" is required.'); } @@ -344,6 +347,7 @@ API.v1.addRoute( reply, users: users?.filter(isTruthy) || [], encrypted, + topic, }); return API.v1.success({ discussion }); @@ -385,6 +389,48 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.images', + { authRequired: true, validateParams: isRoomsImagesProps }, + { + async get() { + const room = await Rooms.findOneById>(this.queryParams.roomId, { + projection: { t: 1, teamId: 1, prid: 1 }, + }); + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.unauthorized(); + } + + let initialImage: IUpload | null = null; + if (this.queryParams.startingFromId) { + initialImage = await Uploads.findOneById(this.queryParams.startingFromId); + } + + const { offset, count } = await getPaginationItems(this.queryParams); + + const { cursor, totalCount } = Uploads.findImagesByRoomId(room._id, initialImage?.uploadedAt, { + skip: offset, + limit: count, + }); + + const [files, total] = await Promise.all([cursor.toArray(), totalCount]); + + // If the initial image was not returned in the query, insert it as the first element of the list + if (initialImage && !files.find(({ _id }) => _id === (initialImage as IUpload)._id)) { + files.splice(0, 0, initialImage); + } + + return API.v1.success({ + files, + count, + offset, + total, + }); + }, + }, +); + API.v1.addRoute( 'rooms.adminRooms', { authRequired: true }, @@ -635,3 +681,39 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'rooms.muteUser', + { authRequired: true, validateParams: isRoomsMuteUnmuteUserProps }, + { + async post() { + const user = await getUserFromParams(this.bodyParams); + + if (!user.username) { + return API.v1.failure('Invalid user'); + } + + await muteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'rooms.unmuteUser', + { authRequired: true, validateParams: isRoomsMuteUnmuteUserProps }, + { + async post() { + const user = await getUserFromParams(this.bodyParams); + + if (!user.username) { + return API.v1.failure('Invalid user'); + } + + await unmuteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 011988f5ba2e..bccfc8d91fc7 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -106,7 +106,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true }, { async post() { - if (!this.bodyParams.name || !this.bodyParams.name.trim()) { + if (!this.bodyParams.name?.trim()) { throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); } diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 10ea2f0b5ac2..ccca23f8ea82 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -6,6 +6,8 @@ import { isUserSetActiveStatusParamsPOST, isUserDeactivateIdleParamsPOST, isUsersInfoParamsGetProps, + isUsersListStatusProps, + isUsersSendWelcomeEmailProps, isUserRegisterParamsPOST, isUserLogoutParamsPOST, isUsersListTeamsProps, @@ -24,6 +26,7 @@ import type { Filter } from 'mongodb'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; +import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail'; import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; @@ -40,6 +43,7 @@ import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; +import { generateAccessToken } from '../../../lib/server/methods/createToken'; import { settings } from '../../../settings/server'; import { getURL } from '../../../utils/server/getURL'; import { API } from '../api'; @@ -48,7 +52,7 @@ import { getUserFromParams } from '../helpers/getUserFromParams'; import { isUserFromParams } from '../helpers/isUserFromParams'; import { getUploadFormData } from '../lib/getUploadFormData'; import { isValidQuery } from '../lib/isValidQuery'; -import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; +import { findPaginatedUsersByStatus, findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; API.v1.addRoute( 'users.getAvatar', @@ -555,6 +559,60 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'users.listByStatus', + { + authRequired: true, + validateParams: isUsersListStatusProps, + permissionsRequired: ['view-d-room'], + }, + { + async get() { + if ( + settings.get('API_Apply_permission_view-outside-room_on_users-list') && + !(await hasPermissionAsync(this.userId, 'view-outside-room')) + ) { + return API.v1.unauthorized(); + } + + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + const { status, hasLoggedIn, type, roles, searchTerm } = this.queryParams; + + return API.v1.success( + await findPaginatedUsersByStatus({ + uid: this.userId, + offset, + count, + sort, + status, + roles, + searchTerm, + hasLoggedIn, + type, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'users.sendWelcomeEmail', + { + authRequired: true, + validateParams: isUsersSendWelcomeEmailProps, + permissionsRequired: ['send-mail'], + }, + { + async post() { + const { email } = this.bodyParams; + await sendWelcomeEmail(email); + + return API.v1.success(); + }, + }, +); + API.v1.addRoute( 'users.register', { @@ -636,11 +694,13 @@ API.v1.addRoute( API.v1.addRoute( 'users.createToken', - { authRequired: true }, + { authRequired: true, deprecationVersion: '8.0.0' }, { async post() { const user = await getUserFromParams(this.bodyParams); - const data = await Meteor.callAsync('createToken', user._id); + + const data = await generateAccessToken(this.userId, user._id); + return data ? API.v1.success({ data }) : API.v1.unauthorized(); }, }, diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index b1ee4fd14521..ab2632c912b0 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -7,6 +7,7 @@ export class AppListenerBridge { } async handleEvent(event, ...payload) { + // eslint-disable-next-line complexity const method = (() => { switch (event) { case AppInterface.IPreMessageSentPrevent: diff --git a/apps/meteor/app/authentication/server/index.ts b/apps/meteor/app/authentication/server/index.ts index 6bca5708d5d2..5ff4af4eef11 100644 --- a/apps/meteor/app/authentication/server/index.ts +++ b/apps/meteor/app/authentication/server/index.ts @@ -1,3 +1,2 @@ import './hooks/login'; - -export * from './startup'; +import './startup'; diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index e3b97c1aae88..cc5a04c275b7 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -1,3 +1,5 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import { User } from '@rocket.chat/core-services'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; @@ -5,12 +7,12 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateUserCallback } from '../../../../lib/callbacks/beforeCreateUserCallback'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; +import { getMaxLoginTokens } from '../../../../server/lib/getMaxLoginTokens'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; @@ -263,10 +265,14 @@ Accounts.onCreateUser(function (...args) { const { insertUserDoc } = Accounts; const insertUserDocAsync = async function (options, user) { - const globalRoles = []; + const globalRoles = new Set(); + + if (Match.test(options.globalRoles, [String]) && options.globalRoles.length > 0) { + options.globalRoles.map((role) => globalRoles.add(role)); + } if (Match.test(user.globalRoles, [String]) && user.globalRoles.length > 0) { - globalRoles.push(...user.globalRoles); + user.globalRoles.map((role) => globalRoles.add(role)); } delete user.globalRoles; @@ -275,11 +281,12 @@ const insertUserDocAsync = async function (options, user) { const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); if (defaultAuthServiceRoles.length > 0) { - globalRoles.push(...defaultAuthServiceRoles); + defaultAuthServiceRoles.map((role) => globalRoles.add(role)); } } - const roles = getNewUserRoles(globalRoles); + const arrayGlobalRoles = [...globalRoles]; + const roles = options.skipNewUserRolesSetting ? arrayGlobalRoles : getNewUserRoles(arrayGlobalRoles); if (!user.type) { user.type = 'user'; @@ -324,7 +331,7 @@ const insertUserDocAsync = async function (options, user) { await addUserRolesAsync(_id, roles); // Make user's roles to be present on callback - user = await Users.findOneById(_id, { projection: { username: 1, type: 1 } }); + user = await Users.findOneById(_id, { projection: { username: 1, type: 1, roles: 1 } }); if (user.username) { if (options.joinDefaultChannels !== false) { @@ -350,8 +357,8 @@ const insertUserDocAsync = async function (options, user) { if (!options.skipAppsEngineEvent) { // `post` triggered events don't need to wait for the promise to resolve - Apps.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }).catch((e) => { - Apps.getRocketChatLogger().error('Error while executing post user created event:', e); + Apps.self?.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }).catch((e) => { + Apps.self?.getRocketChatLogger().error('Error while executing post user created event:', e); }); } @@ -424,7 +431,7 @@ const validateLoginAttemptAsync = async function (login) { */ if (login.type !== 'resume') { // App IPostUserLoggedIn event hook - await Apps.triggerEvent(AppEvents.IPostUserLoggedIn, login.user); + await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedIn, login.user); } return true; @@ -475,20 +482,14 @@ Accounts.validateNewUser((user) => { return true; }); -export const MAX_RESUME_LOGIN_TOKENS = parseInt(process.env.MAX_RESUME_LOGIN_TOKENS) || 50; - Accounts.onLogin(async ({ user }) => { if (!user || !user.services || !user.services.resume || !user.services.resume.loginTokens || !user._id) { return; } - if (user.services.resume.loginTokens.length < MAX_RESUME_LOGIN_TOKENS) { + if (user.services.resume.loginTokens.length < getMaxLoginTokens()) { return; } - const { tokens } = (await Users.findAllResumeTokensByUserId(user._id))[0]; - if (tokens.length >= MAX_RESUME_LOGIN_TOKENS) { - const oldestDate = tokens.reverse()[MAX_RESUME_LOGIN_TOKENS - 1]; - await Users.removeOlderResumeTokensByUserId(user._id, oldestDate.when); - } + await User.ensureLoginTokensLimit(user._id); }); diff --git a/apps/meteor/app/authorization/lib/AuthorizationUtils.ts b/apps/meteor/app/authorization/lib/AuthorizationUtils.ts index 41c96bd6fd3a..6ad5cab04720 100644 --- a/apps/meteor/app/authorization/lib/AuthorizationUtils.ts +++ b/apps/meteor/app/authorization/lib/AuthorizationUtils.ts @@ -31,7 +31,7 @@ export const AuthorizationUtils = class { } const rules = restrictedRolePermissions.get(roleId); - if (!rules || !rules.size) { + if (!rules?.size) { return false; } diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 46b858c4d3ea..1cf02277878a 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -17,6 +17,7 @@ import { } from '../../../../client/views/room/MessageList/lib/autoTranslate'; import { hasPermission } from '../../../authorization/client'; import { Subscriptions, Messages } from '../../../models/client'; +import { settings } from '../../../settings/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; let userLanguage = 'en'; @@ -102,7 +103,7 @@ export const AutoTranslate = { Tracker.autorun(async (c) => { const uid = Meteor.userId(); - if (!uid || !hasPermission('auto-translate')) { + if (!settings.get('AutoTranslate_Enabled') || !uid || !hasPermission('auto-translate')) { return; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts index 92ff94a4b8f0..3a04031ebb88 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts @@ -45,7 +45,7 @@ export async function getWorkspaceAccessTokenWithScope(scope = '', throwOnError headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', body, - timeout: 3000, + timeout: 5000, }); payload = await response.json(); diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts index d6815a7dcc15..cb2d19cfb92e 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts @@ -1,6 +1,6 @@ +import { applyLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; -import { applyLicense } from '../../../../ee/app/license/server/applyLicense'; import { settings } from '../../../settings/server'; import { syncCloudData } from './syncWorkspace/syncCloudData'; diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index dd933107d57c..473acef88c29 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -103,7 +103,7 @@ const getSupportedVersionsFromCloud = async () => { const response = await handleResponse( fetch(releaseEndpoint, { headers, - timeout: 3000, + timeout: 5000, }), ); diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts index 2bff8e1526d2..91202d973170 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts @@ -98,7 +98,7 @@ const fetchWorkspaceClientPayload = async ({ Authorization: `Bearer ${token}`, }, body: workspaceRegistrationData, - timeout: 3000, + timeout: 5000, }); if (!response.ok) { diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 8e43ba49f6d2..309053014016 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -7,7 +7,6 @@ import { Logger } from '@rocket.chat/logger'; import { Meteor } from 'meteor/meteor'; import type { StaticFiles } from 'meteor/webapp'; import { WebApp, WebAppInternals } from 'meteor/webapp'; -import _ from 'underscore'; import { settings } from '../../settings/server'; @@ -16,14 +15,28 @@ type NextFunction = (err?: any) => void; const logger = new Logger('CORS'); +let templatePromise: Promise | void; + +declare module 'meteor/webapp' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebApp { + function setInlineScriptsAllowed(allowed: boolean): Promise; + } +} + settings.watch( 'Enable_CSP', - Meteor.bindEnvironment((enabled) => { - WebAppInternals.setInlineScriptsAllowed(!enabled); + Meteor.bindEnvironment(async (enabled) => { + templatePromise = WebAppInternals.setInlineScriptsAllowed(!enabled); }), ); -WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { +WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { + if (templatePromise) { + await templatePromise; + templatePromise = void 0; + } + // XSS Protection for old browsers (IE) res.setHeader('X-XSS-Protection', '1'); @@ -160,7 +173,7 @@ WebApp.httpServer.addListener('request', (req, res, ...args) => { const isLocal = localhostRegexp.test(remoteAddress) && - (!req.headers['x-forwarded-for'] || _.all((req.headers['x-forwarded-for'] as string).split(','), localhostTest)); + (!req.headers['x-forwarded-for'] || (req.headers['x-forwarded-for'] as string).split(',').every(localhostTest)); // @ts-expect-error - `pair` is valid, but doesnt exists on types const isSsl = req.connection.pair || (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'].indexOf('https') !== -1); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 6d60e9af24bc..18b42ba1a31f 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -62,6 +62,7 @@ type CreateDiscussionProperties = { users: Array>; user: IUser; encrypted?: boolean; + topic?: string; }; const create = async ({ @@ -72,6 +73,7 @@ const create = async ({ users, user, encrypted, + topic, }: CreateDiscussionProperties): Promise => { // if you set both, prid and pmid, and the rooms dont match... should throw an error) let message: null | IMessage = null; @@ -145,7 +147,7 @@ const create = async ({ const type = await roomCoordinator.getRoomDirectives(parentRoom.t).getDiscussionType(parentRoom); const description = parentRoom.encrypted ? '' : message?.msg; - const topic = parentRoom.name; + const discussionTopic = topic || parentRoom.name; if (!type) { throw new Meteor.Error('error-invalid-type', 'Cannot define discussion room type', { @@ -163,7 +165,7 @@ const create = async ({ { fname: discussionName, description, // TODO discussions remove - topic, // TODO discussions remove + topic: discussionTopic, prid, encrypted, }, @@ -203,7 +205,7 @@ declare module '@rocket.chat/ui-contexts' { export const createDiscussion = async ( userId: string, - { prid, pmid, t_name: discussionName, reply, users, encrypted }: Omit, + { prid, pmid, t_name: discussionName, reply, users, encrypted, topic }: Omit, ): Promise< IRoom & { rid: string; @@ -229,7 +231,7 @@ export const createDiscussion = async ( }); } - return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted }); + return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted, topic }); }; Meteor.methods({ diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 42e9a3a99553..1a98ce857f01 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -12,6 +12,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import * as banners from '../../../client/lib/banners'; import type { LegacyBannerPayload } from '../../../client/lib/banners'; import { imperativeModal } from '../../../client/lib/imperativeModal'; +import { dispatchToastMessage } from '../../../client/lib/toast'; import { mapMessageFromApi } from '../../../client/lib/utils/mapMessageFromApi'; import { waitUntilFind } from '../../../client/lib/utils/waitUntilFind'; import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordModal'; @@ -223,6 +224,7 @@ class E2E extends Emitter { onConfirm: () => { Meteor._localStorage.removeItem('e2e.randomPassword'); this.closeAlert(); + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Set') }); imperativeModal.close(); }, }, diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 35bb62cebe94..4458f9d61881 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -8,6 +8,7 @@ import stream from 'stream'; import URL from 'url'; import { hashLoginToken } from '@rocket.chat/account-utils'; +import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import type { IUpload } from '@rocket.chat/core-typings'; import { Users, Avatars, UserDataFiles, Uploads, Settings, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; @@ -21,7 +22,6 @@ import sharp from 'sharp'; import type { WritableStreamBuffer } from 'stream-buffers'; import streamBuffers from 'stream-buffers'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; @@ -177,7 +177,7 @@ export const FileUpload = { // App IPreFileUpload event hook try { - await Apps.triggerEvent(AppEvents.IPreFileUpload, { file, content: content || Buffer.from([]) }); + await Apps.self?.triggerEvent(AppEvents.IPreFileUpload, { file, content: content || Buffer.from([]) }); } catch (error: any) { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); @@ -587,15 +587,7 @@ export const FileUpload = { } // eslint-disable-next-line prettier/prettier - const headersToProxy = [ - 'age', - 'cache-control', - 'content-length', - 'content-type', - 'date', - 'expired', - 'last-modified', - ]; + const headersToProxy = ['age', 'cache-control', 'content-length', 'content-type', 'date', 'expired', 'last-modified']; headersToProxy.forEach((header) => { fileRes.headers[header] && res.setHeader(header, String(fileRes.headers[header])); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 3267e557854f..e4ca2ab729d8 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -1,4 +1,12 @@ -import type { MessageAttachment, FileAttachmentProps, IUser, IUpload, AtLeast, FilesAndAttachments } from '@rocket.chat/core-typings'; +import type { + MessageAttachment, + FileAttachmentProps, + IUser, + IUpload, + AtLeast, + FilesAndAttachments, + IMessage, +} from '@rocket.chat/core-typings'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; @@ -177,6 +185,7 @@ export const sendFileMessage = async ( groupable: Match.Optional(Boolean), msg: Match.Optional(String), tmid: Match.Optional(String), + customFields: Match.Optional(String), }), ); @@ -188,7 +197,8 @@ export const sendFileMessage = async ( file: files[0], files, attachments, - ...msgData, + ...(msgData as Partial), + ...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }), msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, }); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index f5315b4f1e6c..493d14061bf2 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -502,6 +502,7 @@ export class ImportDataConverter { } const userId = await this.insertUser(data); + data._id = userId; insertedIds.add(userId); if (!this._options.skipDefaultChannels) { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 563022ea5a6a..b6d977dc36e2 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -4,6 +4,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getDefaultChannels } from './getDefaultChannels'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { @@ -23,6 +24,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: groupMentions: 0, ...(room.favorite && { f: true }), ...autoTranslateConfig, + ...getDefaultSubscriptionPref(user), }); // Insert user joined message diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 4e29576cf3bb..1fc80c6f3a44 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -1,3 +1,4 @@ +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'; @@ -5,10 +6,10 @@ import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; export const addUserToRoom = async function ( rid: string, @@ -54,7 +55,7 @@ export const addUserToRoom = async function ( } try { - await Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); } catch (error: any) { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); @@ -81,6 +82,7 @@ export const addUserToRoom = async function ( userMentions: 1, groupMentions: 0, ...autoTranslateConfig, + ...getDefaultSubscriptionPref(userToBeAdded as IUser), }); if (!userToBeAdded.username) { @@ -118,7 +120,7 @@ export const addUserToRoom = async function ( // Keep the current event await callbacks.run('afterJoinRoom', userToBeAdded, room); - void Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); + void Apps.self?.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); }); } diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 28bb74d7abe9..c1de81332543 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -1,3 +1,4 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import type { ISubscriptionExtraData } from '@rocket.chat/core-services'; import type { ICreatedRoom, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; @@ -6,7 +7,6 @@ import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; import type { MatchKeysAndValues } from 'mongodb'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; import { settings } from '../../../settings/server'; @@ -103,7 +103,7 @@ export async function createDirectRoom( _USERNAMES: usernames, }; - const prevent = await Apps.triggerEvent('IPreRoomCreatePrevent', tmpRoom).catch((error) => { + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmpRoom).catch((error) => { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -115,7 +115,10 @@ export async function createDirectRoom( throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.'); } - const result = await Apps.triggerEvent('IPreRoomCreateModify', await Apps.triggerEvent('IPreRoomCreateExtend', tmpRoom)); + const result = await Apps.self?.triggerEvent( + AppEvents.IPreRoomCreateModify, + await Apps.self?.triggerEvent(AppEvents.IPreRoomCreateExtend, tmpRoom), + ); if (typeof result === 'object') { Object.assign(roomInfo, result); @@ -169,7 +172,7 @@ export async function createDirectRoom( await callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator }); - void Apps.triggerEvent('IPostRoomCreate', insertedRoom); + void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, insertedRoom); } return { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index f3c6730b1dfe..2d0b94198d96 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,4 +1,5 @@ /* eslint-disable complexity */ +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 type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; @@ -6,10 +7,10 @@ import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typ import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { createDirectRoom } from './createDirectRoom'; @@ -36,14 +37,14 @@ async function createUsersSubscriptions({ options?: ICreateRoomParams['options']; }) { if (shouldBeHandledByFederation) { - const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; - extra.ls = now; - extra.roles = ['owner']; - - if (room.prid) { - extra.prid = room.prid; - } + const extra: Partial = { + ...options?.subscriptionExtra, + open: true, + ls: now, + roles: ['owner'], + ...(room.prid && { prid: room.prid }), + ...getDefaultSubscriptionPref(owner), + }; await Subscriptions.createWithRoomAndUser(room, owner, extra); @@ -198,7 +199,7 @@ export const createRoom = async ( _USERNAMES: members, }; - const prevent = await Apps.triggerEvent('IPreRoomCreatePrevent', tmp).catch((error) => { + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmp).catch((error) => { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -210,7 +211,10 @@ export const createRoom = async ( throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.'); } - const eventResult = await Apps.triggerEvent('IPreRoomCreateModify', await Apps.triggerEvent('IPreRoomCreateExtend', tmp)); + const eventResult = await Apps.self?.triggerEvent( + AppEvents.IPreRoomCreateModify, + await Apps.triggerEvent(AppEvents.IPreRoomCreateExtend, tmp), + ); if (eventResult && typeof eventResult === 'object' && delete eventResult._USERNAMES) { Object.assign(roomProps, eventResult); @@ -242,7 +246,7 @@ export const createRoom = async ( callbacks.runAsync('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members }); } - void Apps.triggerEvent('IPostRoomCreate', room); + void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, room); return { rid: room._id, // backwards compatible inserted: true, diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index cd4456b24514..9368787bf7ea 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,9 +1,9 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages'; import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage'; @@ -29,14 +29,14 @@ export const deleteMessageValidatingPermission = async (message: AtLeast { - const deletedMsg = await Messages.findOneById(message._id); + const deletedMsg: IMessage | null = await Messages.findOneById(message._id); const isThread = (deletedMsg?.tcount || 0) > 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; const showDeletedStatus = settings.get('Message_ShowDeletedStatus') || isThread; - const bridges = Apps?.isLoaded() && Apps.getBridges(); + const bridges = Apps.self?.isLoaded() && Apps.getBridges(); if (deletedMsg && bridges) { - const prevent = await bridges.getListenerBridge().messageEvent('IPreMessageDeletePrevent', deletedMsg); + const prevent = await bridges.getListenerBridge().messageEvent(AppEvents.IPreMessageDeletePrevent, deletedMsg); if (prevent) { throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); } @@ -95,7 +95,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { if (Array.isArray(message.attachments) && message.attachments.length) { validateBodyAttachments(message.attachments); } + + if (message.customFields) { + validateCustomMessageFields({ + customFields: message.customFields, + messageCustomFieldsEnabled: settings.get('Message_CustomFields_Enabled'), + messageCustomFields: settings.get('Message_CustomFields'), + }); + } }; export function prepareMessageObject( @@ -216,7 +224,7 @@ export const sendMessage = async function (user: any, message: any, room: any, u prepareMessageObject(message, room._id, user); if (message.t === 'otr') { - notifications.streamRoomMessage.emit(message.rid, message, user, room); + void api.broadcast('otrMessage', { roomId: message.rid, message, user, room }); return message; } @@ -225,7 +233,7 @@ export const sendMessage = async function (user: any, message: any, room: any, u } // For the Rocket.Chat Apps :) - if (Apps?.isLoaded()) { + if (Apps.self?.isLoaded()) { const listenerBridge = Apps.getBridges()?.getListenerBridge(); const prevent = await listenerBridge?.messageEvent('IPreMessageSentPrevent', message); @@ -275,7 +283,7 @@ export const sendMessage = async function (user: any, message: any, room: any, u message._id = insertedId; } - if (Apps?.isLoaded()) { + if (Apps.self?.isLoaded()) { // This returns a promise, but it won't mutate anything about the message // so, we don't really care if it is successful or fails void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageSent', message); diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 5cfe29ef41ae..8fdfc964db4e 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -1,12 +1,13 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; -import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; +import type { IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages'; import { settings } from '../../../settings/server'; +import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; export const updateMessage = async function ( @@ -16,31 +17,33 @@ export const updateMessage = async function ( previewUrls?: string[], ): Promise { const originalMessage = originalMsg || (await Messages.findOneById(message._id)); + if (!originalMessage) { + throw new Error('Invalid message ID.'); + } - // For the Rocket.Chat Apps :) - if (message && Apps && Apps.isLoaded()) { - const appMessage = Object.assign({}, originalMessage, message); + let messageData: IMessage = Object.assign({}, originalMessage, message); - const prevent = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedPrevent', appMessage); + // For the Rocket.Chat Apps :) + if (message && Apps.self && Apps.isLoaded()) { + const prevent = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedPrevent, messageData); if (prevent) { throw new Meteor.Error('error-app-prevented-updating', 'A Rocket.Chat App prevented the message updating.'); } - let result; - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedExtend', appMessage); - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedModify', result); + let result = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedExtend, messageData); + result = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedModify, result); if (typeof result === 'object') { - message = Object.assign(appMessage, result); + Object.assign(messageData, result); } } // If we keep history of edits, insert a new message to store history information if (settings.get('Message_KeepHistory')) { - await Messages.cloneAndSaveAsHistoryById(message._id, user as Required>); + await Messages.cloneAndSaveAsHistoryById(messageData._id, user as Required>); } - Object.assign, Omit>(message, { + Object.assign(messageData, { editedAt: new Date(), editedBy: { _id: user._id, @@ -48,17 +51,24 @@ export const updateMessage = async function ( }, }); - parseUrlsInMessage(message, previewUrls); + parseUrlsInMessage(messageData, previewUrls); - const room = await Rooms.findOneById(message.rid); + const room = await Rooms.findOneById(messageData.rid); if (!room) { return; } - // TODO remove type cast - message = await Message.beforeSave({ message: message as IMessage, room, user }); + messageData = await Message.beforeSave({ message: messageData, room, user }); + + if (messageData.customFields) { + validateCustomMessageFields({ + customFields: messageData.customFields, + messageCustomFieldsEnabled: settings.get('Message_CustomFields_Enabled'), + messageCustomFields: settings.get('Message_CustomFields'), + }); + } - const { _id, ...editedMessage } = message; + const { _id, ...editedMessage } = messageData; if (!editedMessage.msg) { delete editedMessage.md; @@ -75,10 +85,10 @@ export const updateMessage = async function ( }, ); - if (Apps?.isLoaded()) { + if (Apps.self?.isLoaded()) { // This returns a promise, but it won't mutate anything about the message // so, we don't really care if it is successful or fails - void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageUpdated', message); + void Apps.getBridges()?.getListenerBridge().messageEvent(AppEvents.IPostMessageUpdated, messageData); } setImmediate(async () => { diff --git a/apps/meteor/app/lib/server/lib/validateCustomMessageFields.ts b/apps/meteor/app/lib/server/lib/validateCustomMessageFields.ts new file mode 100644 index 000000000000..b0126fa07ed6 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/validateCustomMessageFields.ts @@ -0,0 +1,44 @@ +import Ajv from 'ajv'; +import mem from 'mem'; + +const ajv = new Ajv(); + +const customFieldsValidate = mem( + (customFieldsSetting: string) => { + const schema = JSON.parse(customFieldsSetting); + + if (schema.type && schema.type !== 'object') { + throw new Error('Invalid custom fields config'); + } + + return ajv.compile({ + ...schema, + type: 'object', + additionalProperties: false, + }); + }, + { maxAge: 1000 * 60 }, +); + +export const validateCustomMessageFields = ({ + customFields, + messageCustomFieldsEnabled, + messageCustomFields, +}: { + customFields: Record; + messageCustomFieldsEnabled: boolean; + messageCustomFields: string; +}) => { + // get the json schema for the custom fields of the message and validate it using ajv + // if the validation fails, throw an error + // if there are no custom fields, the message object remains unchanged + + if (messageCustomFieldsEnabled !== true) { + throw new Error('Custom fields not enabled'); + } + + const validate = customFieldsValidate(messageCustomFields); + if (!validate(customFields)) { + throw new Error('Invalid custom fields'); + } +}; diff --git a/apps/meteor/app/lib/server/methods/createToken.ts b/apps/meteor/app/lib/server/methods/createToken.ts index b16a61a7dacd..ed665944415a 100644 --- a/apps/meteor/app/lib/server/methods/createToken.ts +++ b/apps/meteor/app/lib/server/methods/createToken.ts @@ -1,8 +1,10 @@ +import { User } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; 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/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -11,22 +13,34 @@ declare module '@rocket.chat/ui-contexts' { } } +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 token = Accounts._generateStampedLoginToken(); + Accounts._insertLoginToken(userId, token); + + await User.ensureLoginTokensLimit(userId); + + return { + userId, + authToken: token.token, + }; +} + Meteor.methods({ async createToken(userId) { - const uid = Meteor.userId(); - - if ( - !['yes', 'true'].includes(String(process.env.CREATE_TOKENS_FOR_USERS)) || - !uid || - (uid !== userId && !(await hasPermissionAsync(uid, 'user-generate-access-token'))) - ) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'createToken' }); + methodDeprecationLogger.method('createToken', '8.0.0'); + + const callee = Meteor.userId(); + if (!callee) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createToken' }); } - const token = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, token); - return { - userId, - authToken: token.token, - }; + + return generateAccessToken(callee, userId); }, }); diff --git a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts index ed9929622c6d..2d651950da19 100644 --- a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts +++ b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Users } from '@rocket.chat/models'; import { SHA256 } from '@rocket.chat/sha256'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -5,7 +6,6 @@ import { Accounts } from 'meteor/accounts-base'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; import { deleteUser } from '../functions/deleteUser'; @@ -66,7 +66,7 @@ Meteor.methods({ await deleteUser(uid, confirmRelinquish); // App IPostUserDeleted event hook - await Apps.triggerEvent(AppEvents.IPostUserDeleted, { user }); + await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user }); return true; }, diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index a34492400130..277841fd58e7 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -10,7 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { settings } from '../../../settings/server'; import { updateMessage } from '../functions/updateMessage'; -const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg']; +const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields']; export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { const originalMessage = await Messages.findOneById(message._id); diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 160defcb94ed..962691a78bd8 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -62,7 +62,7 @@ callbacks.add( return message; } - const mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); + const mentions = message.mentions.filter(({ _id, type }) => _id !== 'all' && _id !== 'here' && type !== 'team'); if (!mentions.length) { return message; } diff --git a/apps/meteor/app/livechat/client/index.ts b/apps/meteor/app/livechat/client/index.ts index 84be06f4eb16..bc00a95bcdcd 100644 --- a/apps/meteor/app/livechat/client/index.ts +++ b/apps/meteor/app/livechat/client/index.ts @@ -1,6 +1,5 @@ import '../lib/messageTypes'; import './voip'; import './ui'; -import './startup/notifyUnreadRooms'; import './stylesheets/livechat.css'; import './externalFrame'; diff --git a/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js b/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js deleted file mode 100644 index a758c72cadd8..000000000000 --- a/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { CustomSounds } from '../../../custom-sounds/client'; -import { Subscriptions, Users } from '../../../models/client'; -import { settings } from '../../../settings/client'; -import { getUserPreference } from '../../../utils/client'; - -let audio = null; - -Meteor.startup(() => { - Tracker.autorun(async () => { - if (!settings.get('Livechat_continuous_sound_notification_new_livechat_room')) { - audio && audio.pause(); - return; - } - - const subs = await Subscriptions.find({ t: 'l', ls: { $exists: false }, open: true }).count(); - if (subs === 0) { - audio && audio.pause(); - return; - } - - const user = await Users.findOne( - { _id: Meteor.userId() }, - { - projection: { - 'settings.preferences.newRoomNotification': 1, - }, - }, - ); - - const newRoomNotification = getUserPreference(user, 'newRoomNotification'); - - audio = CustomSounds.play(newRoomNotification, { loop: true }); - }); -}); diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 816c298f0a05..252a83855700 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -3,7 +3,6 @@ import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/model import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; -import { LivechatEnterprise } from '../../../../../ee/app/livechat-enterprise/server/lib/LivechatEnterprise'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; @@ -17,6 +16,7 @@ import { } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; +import { isDepartmentCreationAvailable } from '../../../server/lib/isDepartmentCreationAvailable'; API.v1.addRoute( 'livechat/department', @@ -60,7 +60,7 @@ API.v1.addRoute( }); const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const department = await LivechatEnterprise.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents); + const department = await LivechatTs.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents); if (department) { return API.v1.success({ @@ -122,7 +122,7 @@ API.v1.addRoute( } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await LivechatEnterprise.saveDepartment(_id, department, agentParam); + await LivechatTs.saveDepartment(_id, department, agentParam); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), @@ -302,8 +302,8 @@ API.v1.addRoute( }, { async get() { - const isDepartmentCreationAvailable = await LivechatEnterprise.isDepartmentCreationAvailable(); - return API.v1.success({ isDepartmentCreationAvailable }); + const available = await isDepartmentCreationAvailable(); + return API.v1.success({ isDepartmentCreationAvailable: available }); }, }, ); diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts index 8118f353b167..07c69a22d08f 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts @@ -75,7 +75,10 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesQueuedParams, - deprecationVersion: '7.0.0', + deprecation: { + version: '7.0.0', + alternatives: ['livechat/inquiries.queuedForUser'], + }, }, { async get() { diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.ts b/apps/meteor/app/livechat/imports/server/rest/upload.ts index 2d4b021ffd94..14db8f20afcf 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.ts +++ b/apps/meteor/app/livechat/imports/server/rest/upload.ts @@ -14,6 +14,14 @@ API.v1.addRoute('livechat/upload/:rid', { return API.v1.unauthorized(); } + const canUpload = settings.get('Livechat_fileupload_enabled') && settings.get('FileUpload_Enabled'); + + if (!canUpload) { + return API.v1.failure({ + reason: 'error-file-upload-disabled', + }); + } + const visitorToken = this.request.headers['x-visitor-token']; const visitor = await LivechatVisitors.getVisitorByToken(visitorToken as string, {}); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index f610b9a9d3de..2196315ad013 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -230,7 +230,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.transfer', - { validateParams: isPOSTLivechatRoomTransferParams, deprecationVersion: '7.0.0' }, + { validateParams: isPOSTLivechatRoomTransferParams, deprecation: { version: '7.0.0' } }, { async post() { const { rid, token, department } = this.bodyParams; @@ -364,7 +364,9 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['change-livechat-room-visitor'], validateParams: isPUTLivechatRoomVisitorParams, - deprecationVersion: '7.0.0', + deprecation: { + version: '7.0.0', + }, }, { async put() { diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index c541e5f7b2c3..c893cb68ddf7 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -1,13 +1,17 @@ +import type { ILivechatBusinessHour, IBusinessHourTimezone } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import type { AgendaCronJobs } from '@rocket.chat/cron'; -import { LivechatDepartment, Users } from '@rocket.chat/models'; +import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/models'; import moment from 'moment'; -import { closeBusinessHour } from '../../../../ee/app/livechat-enterprise/server/business-hour/Helper'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; +import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; +import { closeBusinessHour } from './closeBusinessHour'; + +const CRON_EVERY_MIDNIGHT_EXPRESSION = '0 0 * * *'; +const CRON_DAYLIGHT_JOB_NAME = 'livechat-business-hour-daylight-saving-time-verifier'; export class BusinessHourManager { private types: Map = new Map(); @@ -29,6 +33,8 @@ export class BusinessHourManager { this.setupCallbacks(); await this.cleanupDisabledDepartmentReferences(); await this.behavior.onStartBusinessHours(); + void this.startDaylightSavingTimeVerifier(); + void this.registerDaylightSavingTimeCronJob(); } async stopManager(): Promise { @@ -36,6 +42,7 @@ export class BusinessHourManager { this.clearCronJobsCache(); this.removeCallbacks(); await this.behavior.onDisableBusinessHours(); + await this.cronJobs.remove(CRON_DAYLIGHT_JOB_NAME); } async restartManager(): Promise { @@ -232,4 +239,52 @@ export class BusinessHourManager { private clearCronJobsCache(): void { this.cronJobsCache = []; } + + hasDaylightSavingTimeChanged(timezone: IBusinessHourTimezone): boolean { + const now = moment().utc().tz(timezone.name); + const currentUTC = now.format('Z'); + const existingTimezoneUTC = moment(timezone.utc, 'Z').utc().tz(timezone.name); + const DSTHasChanged = !moment(currentUTC, 'Z').utc().tz(timezone.name).isSame(existingTimezoneUTC); + + return currentUTC !== timezone.utc && DSTHasChanged; + } + + async registerDaylightSavingTimeCronJob(): Promise { + await this.cronJobs.add(CRON_DAYLIGHT_JOB_NAME, CRON_EVERY_MIDNIGHT_EXPRESSION, this.startDaylightSavingTimeVerifier.bind(this)); + } + + async startDaylightSavingTimeVerifier(): Promise { + const activeBusinessHours = await LivechatBusinessHours.findActiveBusinessHours(); + const timezonesNeedingAdjustment = activeBusinessHours.filter( + ({ timezone }) => timezone && this.hasDaylightSavingTimeChanged(timezone), + ); + if (timezonesNeedingAdjustment.length === 0) { + return; + } + const result = await Promise.allSettled( + timezonesNeedingAdjustment.map((businessHour) => { + const businessHourType = this.getBusinessHourType(businessHour.type); + if (!businessHourType) { + return; + } + + return businessHourType.saveBusinessHour({ + ...businessHour, + timezoneName: businessHour.timezone.name, + workHours: businessHour.workHours.map((hour) => ({ ...hour, start: hour.start.time, finish: hour.finish.time })) as Record< + string, + any + >[], + } as ILivechatBusinessHour & { timezoneName: string }); + }), + ); + const failed = result.filter((r) => r.status === 'rejected'); + if (failed.length > 0) { + failed.forEach((error: any) => { + businessHourLogger.error('Failed to update business hours with new timezone', error.reason); + }); + } + + await this.createCronJobsForWorkHours(); + } } diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e96ccb4c7b89..e50d866aa6b9 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -5,29 +5,9 @@ import moment from 'moment'; import { businessHourLogger } from '../lib/logger'; import { createDefaultBusinessHourRow } from './LivechatBusinessHours'; +import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; -export const filterBusinessHoursThatMustBeOpened = async ( - businessHours: ILivechatBusinessHour[], -): Promise[]> => { - const currentTime = moment(moment().format('dddd:HH:mm:ss'), 'dddd:HH:mm:ss'); - - return businessHours - .filter( - (businessHour) => - businessHour.active && - businessHour.workHours - .filter((hour) => hour.open) - .some((hour) => { - const localTimeStart = moment(`${hour.start.cron.dayOfWeek}:${hour.start.cron.time}:00`, 'dddd:HH:mm:ss'); - const localTimeFinish = moment(`${hour.finish.cron.dayOfWeek}:${hour.finish.cron.time}:00`, 'dddd:HH:mm:ss'); - return currentTime.isSameOrAfter(localTimeStart) && currentTime.isBefore(localTimeFinish); - }), - ) - .map((businessHour) => ({ - _id: businessHour._id, - type: businessHour.type, - })); -}; +export { filterBusinessHoursThatMustBeOpened }; export const filterBusinessHoursThatMustBeOpenedByDay = async ( businessHours: ILivechatBusinessHour[], diff --git a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts new file mode 100644 index 000000000000..a2295b529272 --- /dev/null +++ b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts @@ -0,0 +1,25 @@ +import type { ILivechatBusinessHour, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +import { businessHourLogger } from '../lib/logger'; +import { getAgentIdsForBusinessHour } from './getAgentIdsForBusinessHour'; + +export const closeBusinessHourByAgentIds = async ( + businessHourId: ILivechatBusinessHour['_id'], + agentIds: IUser['_id'][], +): Promise => { + businessHourLogger.debug({ + msg: 'Closing business hour', + businessHour: businessHourId, + totalAgents: agentIds.length, + top10AgentIds: agentIds.slice(0, 10), + }); + await Users.removeBusinessHourByAgentIds(agentIds, businessHourId); + await Users.updateLivechatStatusBasedOnBusinessHours(); +}; + +export const closeBusinessHour = makeFunction(async (businessHour: Pick): Promise => { + const agentIds = await getAgentIdsForBusinessHour(); + return closeBusinessHourByAgentIds(businessHour._id, agentIds); +}); diff --git a/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts new file mode 100644 index 000000000000..7255b3b03792 --- /dev/null +++ b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts @@ -0,0 +1,54 @@ +import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; + +import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; + +describe('different timezones between server and business hours', () => { + beforeEach(() => jest.useFakeTimers().setSystemTime(new Date('2024-04-20T20:10:11Z'))); + afterEach(() => jest.useRealTimers()); + it('should return a bh when the finish time resolves to a different day on server', async () => { + const bh = await filterBusinessHoursThatMustBeOpened([ + { + _id: '65c40fa9052d6750ae25df83', + name: '', + active: true, + type: LivechatBusinessHourTypes.DEFAULT, + workHours: [ + { + day: 'Sunday', + start: { + time: '00:00', + utc: { + dayOfWeek: 'Saturday', + time: '18:30', + }, + cron: { + dayOfWeek: 'Saturday', + time: '15:30', + }, + }, + finish: { + time: '23:59', + utc: { + dayOfWeek: 'Sunday', + time: '18:29', + }, + cron: { + dayOfWeek: 'Sunday', + time: '15:29', + }, + }, + open: true, + code: '', + }, + ], + timezone: { + name: 'Asia/Kolkata', + utc: '+05:30', + }, + ts: new Date(), + }, + ]); + + expect(bh.length).toEqual(1); + }); +}); diff --git a/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts new file mode 100644 index 000000000000..c06dbaac4e18 --- /dev/null +++ b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts @@ -0,0 +1,31 @@ +import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; +import moment from 'moment'; + +export const filterBusinessHoursThatMustBeOpened = async ( + businessHours: ILivechatBusinessHour[], +): Promise[]> => { + const currentTime = moment(moment().format('dddd:HH:mm:ss'), 'dddd:HH:mm:ss'); + + return businessHours + .filter( + (businessHour) => + businessHour.active && + businessHour.workHours + .filter((hour) => hour.open) + .some((hour) => { + const localTimeStart = moment(`${hour.start.cron.dayOfWeek}:${hour.start.cron.time}:00`, 'dddd:HH:mm:ss'); + const localTimeFinish = moment(`${hour.finish.cron.dayOfWeek}:${hour.finish.cron.time}:00`, 'dddd:HH:mm:ss'); + + // The way we create the instances sunday will be the first day of the current week not the next one, that way it will never met isBefore + if (localTimeFinish.isBefore(localTimeStart)) { + localTimeFinish.add(1, 'week'); + } + + return currentTime.isSameOrAfter(localTimeStart) && currentTime.isBefore(localTimeFinish); + }), + ) + .map((businessHour) => ({ + _id: businessHour._id, + type: businessHour.type, + })); +}; diff --git a/apps/meteor/app/livechat/server/business-hour/getAgentIdsForBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/getAgentIdsForBusinessHour.ts new file mode 100644 index 000000000000..849975f45d60 --- /dev/null +++ b/apps/meteor/app/livechat/server/business-hour/getAgentIdsForBusinessHour.ts @@ -0,0 +1,45 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; + +const getAllAgentIdsWithoutDepartment = async (): Promise => { + // Fetch departments with agents excluding archived ones (disabled ones still can be tied to business hours) + // Then find the agents that are not in any of those departments + + const departmentIds = (await LivechatDepartment.findNotArchived({ projection: { _id: 1 } }).toArray()).map(({ _id }) => _id); + + const agentIdsWithDepartment = await LivechatDepartmentAgents.findAllAgentsConnectedToListOfDepartments(departmentIds); + + const agentIdsWithoutDepartment = ( + await Users.findUsersInRolesWithQuery( + 'livechat-agent', + { + _id: { $nin: agentIdsWithDepartment }, + }, + { projection: { _id: 1 } }, + ).toArray() + ).map((user) => user._id); + + return agentIdsWithoutDepartment; +}; + +const getAllAgentIdsWithDepartmentNotConnectedToBusinessHour = async (): Promise => { + const activeDepartmentsWithoutBusinessHour = ( + await LivechatDepartment.findActiveDepartmentsWithoutBusinessHour({ + projection: { _id: 1 }, + }).toArray() + ).map((dept) => dept._id); + + const agentIdsWithDepartmentNotConnectedToBusinessHour = await LivechatDepartmentAgents.findAllAgentsConnectedToListOfDepartments( + activeDepartmentsWithoutBusinessHour, + ); + return agentIdsWithDepartmentNotConnectedToBusinessHour; +}; + +export const getAgentIdsForBusinessHour = async (): Promise => { + const [withoutDepartment, withDepartmentNotConnectedToBusinessHour] = await Promise.all([ + getAllAgentIdsWithoutDepartment(), + getAllAgentIdsWithDepartmentNotConnectedToBusinessHour(), + ]); + + return [...new Set([...withoutDepartment, ...withDepartmentNotConnectedToBusinessHour])]; +}; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index bf575d9e346d..453869d4425a 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; import { api, Message, Omnichannel } from '@rocket.chat/core-services'; import type { @@ -30,7 +31,6 @@ import { import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; @@ -273,7 +273,7 @@ export const removeAgentFromSubscription = async (rid: string, { _id, username } await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name }); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user }); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user }); }); }; @@ -452,7 +452,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T } setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.AGENT, room: rid, from: oldServedBy?._id, @@ -482,7 +482,7 @@ export const updateChatDepartment = async ({ ]); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.DEPARTMENT, room: rid, from: oldDepartmentId, @@ -539,10 +539,24 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi agent = { agentId, username }; } - if (!RoutingManager.getConfig()?.autoAssignAgent || !(await Omnichannel.isWithinMACLimit(room))) { + const department = await LivechatDepartment.findOneById< + Pick + >(departmentId, { + projection: { + allowReceiveForwardOffline: 1, + fallbackForwardDepartment: 1, + name: 1, + }, + }); + + if ( + !RoutingManager.getConfig()?.autoAssignAgent || + !(await Omnichannel.isWithinMACLimit(room)) || + (department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId))) + ) { logger.debug(`Room ${room._id} will be on department queue`); await LivechatTyped.saveTransferHistory(room, transferData); - return RoutingManager.unassignAgent(inquiry, departmentId); + return RoutingManager.unassignAgent(inquiry, departmentId, true); } // Fake the department to forward the inquiry - Case the forward process does not success @@ -559,11 +573,6 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi const { servedBy, chatQueued } = roomTaken; if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) { - const department = departmentId - ? await LivechatDepartment.findOneById>(departmentId, { - projection: { fallbackForwardDepartment: 1, name: 1 }, - }) - : null; if (!department?.fallbackForwardDepartment?.length) { logger.debug(`Cannot forward room ${room._id}. Chat assigned to agent ${servedBy._id} (Previous was ${oldServedBy._id})`); throw new Error('error-no-agents-online-in-department'); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 0a7b29880881..588e3cdac723 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,6 +1,7 @@ import dns from 'dns'; import * as util from 'util'; +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, @@ -19,6 +20,7 @@ import type { IMessageInbox, IOmnichannelAgent, ILivechatDepartmentAgents, + LivechatDepartmentDTO, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -38,11 +40,11 @@ import { import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; import moment from 'moment-timezone'; import type { Filter, FindCursor, UpdateFilter } from 'mongodb'; import UAParser from 'ua-parser-js'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; @@ -63,6 +65,7 @@ import { businessHourManager } from '../business-hour'; import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; +import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -329,8 +332,8 @@ class LivechatClass { * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed * in the next major version of the Apps-Engine */ - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); }); if (process.env.TEST_MODE) { await callbacks.run('livechat.closeRoom', { @@ -1426,7 +1429,7 @@ class LivechatClass { const ret = await LivechatVisitors.saveGuestById(_id, updateData); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); }); return ret; @@ -1792,7 +1795,7 @@ class LivechatClass { await LivechatRooms.saveRoomById(roomData); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); }); if (guestData?.name?.trim().length) { @@ -1807,6 +1810,111 @@ class LivechatClass { return true; } } + + /** + * @param {string|null} _id - The department id + * @param {Partial} departmentData + * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + */ + async saveDepartment( + _id: string | null, + departmentData: LivechatDepartmentDTO, + departmentAgents?: { + upsert?: { agentId: string; count?: number; order?: number }[]; + remove?: { agentId: string; count?: number; order?: number }; + }, + ) { + check(_id, Match.Maybe(String)); + + const department = _id ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; + + if (!department && !(await isDepartmentCreationAvailable())) { + throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { + method: 'livechat:saveDepartment', + }); + } + + if (department?.archived && departmentData.enabled) { + throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { + method: 'livechat:saveDepartment', + }); + } + + const defaultValidations: Record | BooleanConstructor | StringConstructor> = { + enabled: Boolean, + name: String, + description: Match.Optional(String), + showOnRegistration: Boolean, + email: String, + showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), + fallbackForwardDepartment: Match.Optional(String), + departmentsAllowedToForward: Match.Optional([String]), + allowReceiveForwardOffline: Match.Optional(Boolean), + }; + + // The Livechat Form department support addition/custom fields, so those fields need to be added before validating + Object.keys(departmentData).forEach((field) => { + if (!defaultValidations.hasOwnProperty(field)) { + defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); + } + }); + + check(departmentData, defaultValidations); + check( + departmentAgents, + Match.Maybe({ + upsert: Match.Maybe(Array), + remove: Match.Maybe(Array), + }), + ); + + const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error( + 'error-validating-department-chat-closing-tags', + 'At least one closing tag is required when the department requires tag(s) on closing conversations.', + { method: 'livechat:saveDepartment' }, + ); + } + + if (_id && !department) { + throw new Meteor.Error('error-department-not-found', 'Department not found', { + method: 'livechat:saveDepartment', + }); + } + + if (fallbackForwardDepartment === _id) { + throw new Meteor.Error( + 'error-fallback-department-circular', + 'Cannot save department. Circular reference between fallback department and department', + ); + } + + if (fallbackForwardDepartment) { + const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { + projection: { _id: 1, fallbackForwardDepartment: 1 }, + }); + if (!fallbackDep) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { + method: 'livechat:saveDepartment', + }); + } + } + + const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + if (departmentDB && departmentAgents) { + await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); + } + + // Disable event + if (department?.enabled && !departmentDB?.enabled) { + await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + } + + return departmentDB; + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 4569f3da42b8..8be71aa4c991 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -5,7 +6,6 @@ import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper'; import { RoutingManager } from './RoutingManager'; @@ -105,7 +105,7 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } - void Apps.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); await LivechatRooms.updateRoomCount(); await queueInquiry(inquiry, agent); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index f1fe1d506a8a..19437d800ee2 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, @@ -16,7 +17,6 @@ import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@ro import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { @@ -46,7 +46,7 @@ type Routing = { options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>; assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise; - unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise; + unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string, shouldQueue?: boolean): Promise; takeInquiry( inquiry: Omit< ILivechatInquiryRecord, @@ -154,11 +154,11 @@ export const RoutingManager: Routing = { await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); return inquiry; }, - async unassignAgent(inquiry, departmentId) { + async unassignAgent(inquiry, departmentId, shouldQueue = false) { const { rid, department } = inquiry; const room = await LivechatRooms.findOneById(rid); @@ -181,6 +181,10 @@ export const RoutingManager: Routing = { const { servedBy } = room; + if (shouldQueue) { + await LivechatInquiry.queueInquiry(inquiry._id); + } + if (servedBy) { await LivechatRooms.removeAgentByRoomId(rid); await this.removeAllRoomSubscriptions(room); diff --git a/apps/meteor/app/livechat/server/lib/isDepartmentCreationAvailable.ts b/apps/meteor/app/livechat/server/lib/isDepartmentCreationAvailable.ts new file mode 100644 index 000000000000..380dee64e127 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/isDepartmentCreationAvailable.ts @@ -0,0 +1,7 @@ +import { LivechatDepartment } from '@rocket.chat/models'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export const isDepartmentCreationAvailable = makeFunction(async (): Promise => { + // Only one department can exist at a time + return (await LivechatDepartment.countTotal()) === 0; +}); diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index dd83a294cb0e..971c2189f9a7 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -2,8 +2,8 @@ import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import { LivechatEnterprise } from '../../../../ee/app/livechat-enterprise/server/lib/LivechatEnterprise'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -21,6 +21,7 @@ declare module '@rocket.chat/ui-contexts' { chatClosingTags?: string[]; fallbackForwardDepartment?: string; departmentsAllowedToForward?: string[]; + allowReceiveForwardOffline?: boolean; }, departmentAgents?: | { @@ -42,6 +43,6 @@ Meteor.methods({ }); } - return LivechatEnterprise.saveDepartment(_id, departmentData, { upsert: departmentAgents }); + return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents }); }, }); diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index b50fdfd26a2a..e562fc8e7b39 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -1,3 +1,4 @@ +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'; @@ -7,7 +8,6 @@ import { Meteor } from 'meteor/meteor'; import stripHtml from 'string-strip-html'; import _ from 'underscore'; -import { Apps } from '../../../ee/server/apps'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; @@ -170,7 +170,7 @@ export const sendNoWrap = async ({ const email = { to, from, replyTo, subject, html, text, headers }; - const eventResult = await Apps.triggerEvent('IPreEmailSent', { email }); + const eventResult = await Apps.self?.triggerEvent(AppEvents.IPreEmailSent, { email }); setImmediate(() => Email.sendAsync(eventResult || email).catch((e) => console.error(e))); }; diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index 1ed0a172028b..dc17a75a0192 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; import { isQuoteAttachment, isRegisterUser } from '@rocket.chat/core-typings'; import type { IMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; @@ -6,7 +7,6 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { isTruthy } from '../../../lib/isTruthy'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; @@ -129,7 +129,7 @@ Meteor.methods({ } // App IPostMessagePinned event hook - await Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); const msgId = await Message.saveSystemMessage('message_pinned', originalMessage.rid, '', me, { attachments: [ @@ -216,7 +216,7 @@ Meteor.methods({ } // App IPostMessagePinned event hook - await Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); await Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); if (settings.get('Message_Read_Receipt_Store_Users')) { diff --git a/apps/meteor/app/message-star/server/starMessage.ts b/apps/meteor/app/message-star/server/starMessage.ts index 8f025d920057..7ac8fd619d31 100644 --- a/apps/meteor/app/message-star/server/starMessage.ts +++ b/apps/meteor/app/message-star/server/starMessage.ts @@ -1,9 +1,9 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; @@ -57,7 +57,7 @@ Meteor.methods({ await Rooms.updateLastMessageStar(room._id, uid, message.starred); } - await Apps.triggerEvent(AppEvents.IPostMessageStarred, message, await Meteor.userAsync(), message.starred); + await Apps.self?.triggerEvent(AppEvents.IPostMessageStarred, message, await Meteor.userAsync(), message.starred); await Messages.updateUserStarById(message._id, uid, message.starred); diff --git a/apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts b/apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts index acc67681fcdc..f48c40432711 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts @@ -21,6 +21,7 @@ export interface IServiceProviderOptions { metadataTemplate: string; callbackUrl: string; - // The id attribute is filled midway through some operations + // The id and redirectUrl attributes are filled midway through some operations id?: string; + redirectUrl?: string; } 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 f62ab71f2302..76747b599104 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -54,7 +54,7 @@ export class SAML { case 'sloRedirect': return this.processSLORedirectAction(req, res); case 'authorize': - return this.processAuthorizeAction(res, service, samlObject); + return this.processAuthorizeAction(req, res, service, samlObject); case 'validate': return this.processValidateAction(req, res, service, samlObject); default: @@ -378,12 +378,20 @@ export class SAML { } private static async processAuthorizeAction( + req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions, samlObject: ISAMLAction, ): Promise { service.id = samlObject.credentialToken; + // Allow redirecting to internal domains when login process is complete + const { referer } = req.headers; + const siteUrl = settings.get('Site_Url'); + if (typeof referer === 'string' && referer.startsWith(siteUrl)) { + service.redirectUrl = referer; + } + const serviceProvider = new SAMLServiceProvider(service); let url: string | undefined; @@ -430,7 +438,7 @@ export class SAML { }; await this.storeCredential(credentialToken, loginResult); - const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken)); + const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken, service.redirectUrl)); res.writeHead(302, { Location: url, }); 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 70df22120b75..984c7bc458a3 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -131,9 +131,10 @@ export class SAMLUtils { return newTemplate; } - public static getValidationActionRedirectPath(credentialToken: string): string { + public static getValidationActionRedirectPath(credentialToken: string, redirectUrl?: string): string { + const redirectUrlParam = redirectUrl ? `&redirectUrl=${encodeURIComponent(redirectUrl)}` : ''; // the saml_idp_credentialToken param is needed by the mobile app - return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}`; + return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}${redirectUrlParam}`; } public static log(obj: any, ...args: Array): void { diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts index d2d0f85d19ce..9760d521b15a 100644 --- a/apps/meteor/app/oembed/server/providers.ts +++ b/apps/meteor/app/oembed/server/providers.ts @@ -74,7 +74,7 @@ providers.registerProvider({ }); providers.registerProvider({ - urls: [new RegExp('https?://twitter\\.com/[^/]+/status/\\S+')], + urls: [new RegExp('https?://(twitter|x)\\.com/[^/]+/status/\\S+')], endPoint: 'https://publish.twitter.com/oembed', }); diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index 745c70aa4538..a5a502ddf1a2 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -1,9 +1,8 @@ +import { api } from '@rocket.chat/core-services'; import type { IOTRMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import notifications from '../../../notifications/server/lib/Notifications'; - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -16,7 +15,7 @@ Meteor.methods({ if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); } - const acknowlegedMessage: IOTRMessage = { ...message, otrAck: ack }; - notifications.streamRoomMessage.emit(message.rid, acknowlegedMessage); + const acknowledgeMessage: IOTRMessage = { ...message, otrAck: ack }; + void api.broadcast('otrAckUpdate', { roomId: message.rid, acknowledgeMessage }); }, }); diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts new file mode 100644 index 000000000000..819e26e4f003 --- /dev/null +++ b/apps/meteor/app/push/server/fcm.ts @@ -0,0 +1,184 @@ +import EJSON from 'ejson'; +import fetch from 'node-fetch'; +import type { RequestInit, Response } from 'node-fetch'; + +import type { PendingPushNotification } from './definition'; +import { logger } from './logger'; +import type { NativeNotificationParameters } from './push'; + +type FCMDataField = Record; + +type FCMNotificationField = { + title: string; + body: string; + image?: string; +}; + +type FCMMessage = { + notification?: FCMNotificationField; + data?: FCMDataField; + token?: string; + to?: string; + android?: { + collapseKey?: string; + priority?: 'HIGH' | 'NORMAL'; + ttl?: string; + restrictedPackageName?: string; + data?: FCMDataField; + notification?: FCMNotificationField; + fcm_options?: { + analytics_label?: string; + }; + direct_boot_ok?: boolean; + }; + webpush?: { + headers?: FCMDataField; + data?: FCMDataField; + notification?: FCMNotificationField; + fcm_options?: { + link?: string; + analytics_label?: string; + }; + }; + fcm_options?: { + analytics_label?: string; + }; +}; + +// https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode +type FCMError = { + error: { + code: number; + message: string; + status: string; + }; +}; + +/** + * Set at least a 10 second timeout on send requests before retrying. + * Most of FCM's internal Remote Procedure Calls use a 10 second timeout. + * + * Errors: + * - For 400, 401, 403, 404 errors: abort, and do not retry. + * - For 429 errors: retry after waiting for the duration set in the retry-after header. If no retry-after header is set, default to 60 seconds. + * - For 500 errors: retry with exponential backoff. + */ +async function fetchWithRetry(url: string, options: RequestInit, retries = 0): Promise { + const MAX_RETRIES = 5; + const response = await fetch(url, options); + + if (response.ok) { + return response; + } + + if (retries >= MAX_RETRIES) { + logger.error('sendFCM error: max retries reached'); + return response; + } + + const retryAfter = response.headers.get('retry-after'); + const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; + + if (response.status === 429) { + await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); + return fetchWithRetry(url, options, retries + 1); + } + + if (response.status >= 500 && response.status < 600) { + const backoff = Math.pow(2, retries) * 10000; + await new Promise((resolve) => setTimeout(resolve, backoff)); + return fetchWithRetry(url, options, retries + 1); + } + + const error: FCMError = await response.json(); + logger.error('sendFCM error', error); + + return response; +} + +function getFCMMessagesFromPushData(userTokens: string[], notification: PendingPushNotification): { message: FCMMessage }[] { + // first we will get the `data` field from the notification + const data: FCMDataField = notification.payload ? { ejson: EJSON.stringify(notification.payload) } : {}; + + // Set image + if (notification.gcm?.image) { + data.image = notification.gcm?.image; + } + + // Set extra details + if (notification.badge) { + data.msgcnt = notification.badge.toString(); + } + + if (notification.sound) { + data.soundname = notification.sound; + } + + if (notification.notId) { + data.notId = notification.notId.toString(); + } + + if (notification.gcm?.style) { + data.style = notification.gcm?.style; + } + + if (notification.contentAvailable) { + data['content-available'] = notification.contentAvailable.toString(); + } + + // then we will create the notification field + const notificationField: FCMNotificationField = { + title: notification.title, + body: notification.text, + }; + + // then we will create the message + const message: FCMMessage = { + notification: notificationField, + data, + android: { + priority: 'HIGH', + }, + }; + + // then we will create the message for each token + return userTokens.map((token) => ({ message: { ...message, token } })); +} + +export const sendFCM = function ({ userTokens, notification, _replaceToken, _removeToken, options }: NativeNotificationParameters): void { + // We don't use these parameters, but we need to keep them to keep the function signature + // TODO: Remove them when we remove the old sendGCM function + _replaceToken; + _removeToken; + + const tokens = typeof userTokens === 'string' ? [userTokens] : userTokens; + if (!tokens.length) { + logger.log('sendFCM no push tokens found'); + return; + } + + logger.debug('sendFCM', tokens, notification); + + const messages = getFCMMessagesFromPushData(tokens, notification); + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${options.gcm.apiKey}`, + 'access_token_auth': true, + } as Record; + + if (!options.gcm.projectNumber.trim()) { + logger.error('sendFCM error: GCM project number is missing'); + return; + } + + const url = `https://fcm.googleapis.com/v1/projects/${options.gcm.projectNumber}/messages:send`; + + for (const message of messages) { + logger.debug('sendFCM message', message); + const response = fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify(message) }); + + response.catch((err) => { + logger.error('sendFCM error', err); + }); + } +}; diff --git a/apps/meteor/app/push/server/gcm.ts b/apps/meteor/app/push/server/gcm.ts index 6ab679d504f6..192a08ed666e 100644 --- a/apps/meteor/app/push/server/gcm.ts +++ b/apps/meteor/app/push/server/gcm.ts @@ -1,23 +1,13 @@ -import type { IAppsTokens, RequiredField } from '@rocket.chat/core-typings'; import EJSON from 'ejson'; import gcm from 'node-gcm'; -import type { PendingPushNotification, PushOptions } from './definition'; import { logger } from './logger'; +import type { NativeNotificationParameters } from './push'; -export const sendGCM = function ({ - userTokens, - notification, - _replaceToken, - _removeToken, - options, -}: { - userTokens: string | string[]; - notification: PendingPushNotification; - _replaceToken: (currentToken: IAppsTokens['token'], newToken: IAppsTokens['token']) => void; - _removeToken: (token: IAppsTokens['token']) => void; - options: RequiredField; -}) { +/** + * @deprecated Use sendFCM instead, node-gcm is deprecated and google will remove it soon + */ +export const sendGCM = function ({ userTokens, notification, _replaceToken, _removeToken, options }: NativeNotificationParameters) { // Make sure userTokens are an array of strings if (typeof userTokens === 'string') { userTokens = [userTokens]; diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 2594625155fa..760fbea7c232 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -2,17 +2,80 @@ import type { IAppsTokens, RequiredField, Optional, IPushNotificationConfig } fr import { AppsTokens } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { pick } from '@rocket.chat/tools'; +import Ajv from 'ajv'; +import { JWT } from 'google-auth-library'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../settings/server'; import { initAPN, sendAPN } from './apn'; import type { PushOptions, PendingPushNotification } from './definition'; +import { sendFCM } from './fcm'; import { sendGCM } from './gcm'; import { logger } from './logger'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type FCMCredentials = { + type: string; + project_id: string; + private_key_id: string; + private_key: string; + client_email: string; + client_id: string; + auth_uri: string; + token_uri: string; + auth_provider_x509_cert_url: string; + client_x509_cert_url: string; + universe_domain: string; +}; + +export const FCMCredentialsValidationSchema = { + type: 'object', + properties: { + type: { + type: 'string', + }, + project_id: { + type: 'string', + }, + private_key_id: { + type: 'string', + }, + private_key: { + type: 'string', + }, + client_email: { + type: 'string', + }, + client_id: { + type: 'string', + }, + auth_uri: { + type: 'string', + }, + token_uri: { + type: 'string', + }, + auth_provider_x509_cert_url: { + type: 'string', + }, + client_x509_cert_url: { + type: 'string', + }, + universe_domain: { + type: 'string', + }, + }, + required: ['client_email', 'project_id', 'private_key_id', 'private_key'], +}; + +export const isFCMCredentials = ajv.compile(FCMCredentialsValidationSchema); + // This type must match the type defined in the push gateway type GatewayNotification = { uniqueId: string; @@ -58,6 +121,14 @@ type GatewayNotification = { createdBy?: string; }; +export type NativeNotificationParameters = { + userTokens: string | string[]; + notification: PendingPushNotification; + _replaceToken: (currentToken: IAppsTokens['token'], newToken: IAppsTokens['token']) => void; + _removeToken: (token: IAppsTokens['token']) => void; + options: RequiredField; +}; + class PushClass { options: PushOptions = { uniqueId: '', @@ -109,7 +180,12 @@ class PushClass { return Boolean(!!this.options.gateways && settings.get('Register_Server') && settings.get('Cloud_Service_Agree_PrivacyTerms')); } - private sendNotificationNative(app: IAppsTokens, notification: PendingPushNotification, countApn: string[], countGcm: string[]): void { + private async sendNotificationNative( + app: IAppsTokens, + notification: PendingPushNotification, + countApn: string[], + countGcm: string[], + ): Promise { logger.debug('send to token', app.token); if ('apn' in app.token && app.token.apn) { @@ -124,7 +200,29 @@ class PushClass { // Send to GCM // We do support multiple here - so we should construct an array // and send it bulk - Investigate limit count of id's - if (this.options.gcm?.apiKey) { + // TODO: Remove this after the legacy provider is removed + const useLegacyProvider = settings.get('Push_UseLegacy'); + + if (!useLegacyProvider) { + // override this.options.gcm.apiKey with the oauth2 token + const { projectId, token } = await this.getNativeNotificationAuthorizationCredentials(); + const sendGCMOptions = { + ...this.options, + gcm: { + ...this.options.gcm, + apiKey: token, + projectNumber: projectId, + }, + }; + + sendFCM({ + userTokens: app.token.gcm, + notification, + _replaceToken: this.replaceToken, + _removeToken: this.removeToken, + options: sendGCMOptions as RequiredField, + }); + } else if (this.options.gcm?.apiKey) { sendGCM({ userTokens: app.token.gcm, notification, @@ -138,6 +236,37 @@ class PushClass { } } + private async getNativeNotificationAuthorizationCredentials(): Promise<{ token: string; projectId: string }> { + const credentialsString = settings.get('Push_google_api_credentials'); + if (!credentialsString.trim()) { + throw new Error('Push_google_api_credentials is not set'); + } + + try { + const credentials = JSON.parse(credentialsString); + if (!isFCMCredentials(credentials)) { + throw new Error('Push_google_api_credentials is not in the correct format'); + } + + const client = new JWT({ + email: credentials.client_email, + key: credentials.private_key, + keyId: credentials.private_key_id, + scopes: 'https://www.googleapis.com/auth/firebase.messaging', + }); + + await client.authorize(); + + return { + token: client.credentials.access_token as string, + projectId: credentials.project_id, + }; + } catch (error) { + logger.error('Error getting FCM token', error); + throw new Error('Error getting FCM token'); + } + } + private async sendGatewayPush( gateway: string, service: 'apn' | 'gcm', @@ -270,7 +399,7 @@ class PushClass { continue; } - this.sendNotificationNative(app, notification, countApn, countGcm); + await this.sendNotificationNative(app, notification, countApn, countGcm); } if (settings.get('Log_Level') === '2') { diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 27fe4d36a053..36eaab695512 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { api } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; @@ -5,7 +6,6 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { AppEvents, Apps } from '../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; @@ -106,7 +106,7 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction isReacted = true; } - await Apps.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); + await Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); void broadcastMessageFromData({ id: message._id, diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js index 6337b287506a..1d84bead3e85 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js @@ -1,17 +1,18 @@ +import { Apps } from '@rocket.chat/apps'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import { Apps } from '../../../../ee/server/apps'; import { Info } from '../../../utils/rocketchat.info'; export function getAppsStatistics() { return { engineVersion: Info.marketplaceApiVersion, - totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, - totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, + totalInstalled: (Apps.self?.isInitialized() && Apps.getManager().get().length) ?? 0, + totalActive: (Apps.self?.isInitialized() && Apps.getManager().get({ enabled: true }).length) ?? 0, totalFailed: - Apps.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length, + (Apps.self?.isInitialized() && + Apps.getManager() + .get({ disabled: true }) + .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length) ?? + 0, }; } diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/app/statistics/server/lib/getEEStatistics.ts similarity index 80% rename from apps/meteor/ee/app/license/server/getStatistics.ts rename to apps/meteor/app/statistics/server/lib/getEEStatistics.ts index e8ff402ea1ca..ffb5a939c2fd 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getEEStatistics.ts @@ -1,29 +1,15 @@ import { log } from 'console'; import { Analytics } from '@rocket.chat/core-services'; +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'; -type ENTERPRISE_STATISTICS = GenericStats & Partial; - -type GenericStats = { - modules: string[]; - tags: string[]; - seatRequests: number; -}; - -type EEOnlyStats = { - livechatTags: number; - cannedResponses: number; - priorities: number; - slas: number; - businessUnits: number; - omnichannelPdfTranscriptRequested: number; - omnichannelPdfTranscriptSucceeded: number; - omnichannelRoomsWithSlas: number; - omnichannelRoomsWithPriorities: number; - livechatMonitors: number; -}; +type ENTERPRISE_STATISTICS = IStats['enterprise']; + +type GenericStats = Pick; + +type EEOnlyStats = Omit; export async function getStatistics(): Promise { const genericStats: GenericStats = { @@ -42,7 +28,6 @@ export async function getStatistics(): Promise { return statistics; } -// These models are only available on EE license so don't import them inside CE license as it will break the build async function getEEStatistics(): Promise { if (!License.hasModule('livechat-enterprise')) { return; diff --git a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts index 9e50611b3d75..eeb25f0d7f8f 100644 --- a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts @@ -29,6 +29,8 @@ async function getCustomOAuthServices(): Promise< enabled: Boolean(value), mergeRoles: settings.get(`Accounts_OAuth_Custom-${name}-merge_roles`), users: await Users.countActiveUsersByService(name, { readPreference }), + mapChannels: settings.get(`Accounts_OAuth_Custom-${name}-map_channels`), + rolesToSync: !!settings.get(`Accounts_OAuth_Custom-${name}-roles_to_sync`), }, ]; }), @@ -71,6 +73,10 @@ export async function getServicesStatistics(): Promise> generateUsername: settings.get('SAML_Custom_Default_generate_username'), updateSubscriptionsOnLogin: settings.get('SAML_Custom_Default_channels_update'), syncRoles: settings.get('SAML_Custom_Default_role_attribute_sync'), + userDataCustomFieldMap: !( + settings.getSetting('SAML_Custom_Default_user_data_custom_fieldmap')?.packageValue === + settings.getSetting('SAML_Custom_Default_user_data_custom_fieldmap')?.value + ), }, cas: { enabled: settings.get('CAS_enabled'), diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8887f4ce36f2..a6fcc5b17b5b 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -29,7 +29,6 @@ import { import { MongoInternals } from 'meteor/mongo'; import moment from 'moment'; -import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; @@ -40,6 +39,7 @@ import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; import { getMongoInfo } from '../../../utils/server/functions/getMongoInfo'; import { getAppsStatistics } from './getAppsStatistics'; +import { getStatistics as getEnterpriseStatistics } from './getEEStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; @@ -70,20 +70,17 @@ export const statistics = { const statistics = {} as IStats; const statsPms = []; - const fetchWizardSettingValue = async (settingName: string): Promise => { - return ((await Settings.findOne(settingName))?.value as T | undefined) ?? undefined; - }; - // Setup Wizard const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([ - fetchWizardSettingValue('Organization_Type'), - fetchWizardSettingValue('Industry'), - fetchWizardSettingValue('Size'), - fetchWizardSettingValue('Country'), - fetchWizardSettingValue('Language'), - fetchWizardSettingValue('Server_Type'), - fetchWizardSettingValue('Register_Server'), + settings.get('Organization_Type'), + settings.get('Industry'), + settings.get('Size'), + settings.get('Country'), + settings.get('Language'), + settings.get('Server_Type'), + settings.get('Register_Server'), ]); + statistics.wizard = { organizationType, industry, @@ -185,11 +182,13 @@ export const statistics = { ); // Number of custom fields - statsPms.push( - LivechatCustomField.col.count().then((count) => { - statistics.totalCustomFields = count; - }), - ); + statsPms.push((statistics.totalCustomFields = await LivechatCustomField.countDocuments({}))); + + // Number of public custom fields + statsPms.push((statistics.totalLivechatPublicCustomFields = await LivechatCustomField.countDocuments({ public: true }))); + + // Livechat Automatic forwarding feature enabled + statistics.livechatAutomaticForwardingUnansweredChats = settings.get('Livechat_auto_transfer_chat_timeout') !== 0; // Type of routing algorithm used on omnichannel statistics.routingAlgorithm = settings.get('Livechat_Routing_Method') || ''; @@ -567,12 +566,14 @@ export const statistics = { const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; + // Push notification stats // one bit for each of the following: const pushEnabled = settings.get('Push_enable') ? 1 : 0; const pushGatewayEnabled = settings.get('Push_enable_gateway') ? 2 : 0; const pushGatewayChanged = settings.get('Push_gateway') !== defaultGateway ? 4 : 0; statistics.push = pushEnabled | pushGatewayEnabled | pushGatewayChanged; + statistics.pushSecured = settings.get('Push_request_content_from_server'); const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue; statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts index cede3dda33a7..05650d0ad2ef 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.ts +++ b/apps/meteor/app/threads/server/methods/followMessage.ts @@ -1,10 +1,10 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; @@ -44,7 +44,7 @@ Meteor.methods({ const followResult = await follow({ tmid: message.tmid || message._id, uid }); const isFollowed = true; - await Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); + await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); return followResult; }, diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts index c5dad1233173..afc9206b038f 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts @@ -1,10 +1,10 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; @@ -44,7 +44,7 @@ Meteor.methods({ const unfollowResult = await unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); const isFollowed = false; - await Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); + await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); return unfollowResult; }, diff --git a/apps/meteor/app/ui-message/client/ActionManager.ts b/apps/meteor/app/ui-message/client/ActionManager.ts index 733664affd1f..4c892d6d32f2 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.ts +++ b/apps/meteor/app/ui-message/client/ActionManager.ts @@ -3,11 +3,14 @@ import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; import type { RouterContext, IActionManager } from '@rocket.chat/ui-contexts'; import type * as UiKit from '@rocket.chat/ui-kit'; +import { t } from 'i18next'; import type { ContextType } from 'react'; import { lazy } from 'react'; import * as banners from '../../../client/lib/banners'; import { imperativeModal } from '../../../client/lib/imperativeModal'; +import { dispatchToastMessage } from '../../../client/lib/toast'; +import { exhaustiveCheck } from '../../../lib/utils/exhaustiveCheck'; import { sdk } from '../../utils/client/lib/SDKClient'; import { UiKitTriggerTimeoutError } from './UiKitTriggerTimeoutError'; @@ -20,7 +23,7 @@ export class ActionManager implements IActionManager { protected events = new Emitter<{ busy: { busy: boolean }; [viewId: string]: any }>(); - protected triggersId = new Map(); + protected appIdByTriggerId = new Map(); protected viewInstances = new Map< string, @@ -35,8 +38,8 @@ export class ActionManager implements IActionManager { public constructor(protected router: ContextType) {} protected invalidateTriggerId(id: string) { - const appId = this.triggersId.get(id); - this.triggersId.delete(id); + const appId = this.appIdByTriggerId.get(id); + this.appIdByTriggerId.delete(id); return appId; } @@ -66,41 +69,75 @@ export class ActionManager implements IActionManager { public generateTriggerId(appId: string | undefined) { const triggerId = Random.id(); - this.triggersId.set(triggerId, appId); + this.appIdByTriggerId.set(triggerId, appId); setTimeout(() => this.invalidateTriggerId(triggerId), ActionManager.TRIGGER_TIMEOUT); return triggerId; } public async emitInteraction(appId: string, userInteraction: DistributiveOmit) { - this.notifyBusy(); - const triggerId = this.generateTriggerId(appId); - let timeout: ReturnType | undefined; + return this.runWithTimeout( + async () => { + let interaction: UiKit.ServerInteraction | undefined; + + try { + interaction = (await sdk.rest.post(`/apps/ui.interaction/${appId}`, { + ...userInteraction, + triggerId, + })) as UiKit.ServerInteraction; + + this.handleServerInteraction(interaction); + } finally { + switch (userInteraction.type) { + case 'viewSubmit': + if (!!interaction && !['errors', 'modal.update', 'contextual_bar.update'].includes(interaction.type)) + this.disposeView(userInteraction.viewId); + break; + + case 'viewClosed': + if (!!interaction && interaction.type !== 'errors') this.disposeView(userInteraction.payload.viewId); + break; + } + } + }, + { triggerId, appId, ...('viewId' in userInteraction ? { viewId: userInteraction.viewId } : {}) }, + ); + } + + protected async runWithTimeout(task: () => Promise, details: { triggerId: string; appId: string; viewId?: string }) { + this.notifyBusy(); - await Promise.race([ - new Promise((_, reject) => { - timeout = setTimeout(() => reject(new UiKitTriggerTimeoutError('Timeout', { triggerId, appId })), ActionManager.TRIGGER_TIMEOUT); - }), - sdk.rest - .post(`/apps/ui.interaction/${appId}`, { - ...userInteraction, - triggerId, - }) - .then((interaction) => this.handleServerInteraction(interaction)), - ]).finally(() => { - if (timeout) clearTimeout(timeout); + let timer: ReturnType | undefined; + + try { + const taskPromise = task(); + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new UiKitTriggerTimeoutError('Timeout', details)); + }, ActionManager.TRIGGER_TIMEOUT); + }); + + return await Promise.race([taskPromise, timeoutPromise]); + } catch (error) { + if (error instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + if (details.viewId) { + this.disposeView(details.viewId); + } + } + } finally { + if (timer) clearTimeout(timer); this.notifyIdle(); - }); + } } - public handleServerInteraction(interaction: UiKit.ServerInteraction) { + public handleServerInteraction(interaction: UiKit.ServerInteraction): UiKit.ServerInteraction['type'] | undefined { const { triggerId } = interaction; - if (!this.triggersId.has(triggerId)) { - return; - } - const appId = this.invalidateTriggerId(triggerId); if (!appId) { return; @@ -162,8 +199,7 @@ export class ActionManager implements IActionManager { case 'banner.close': { const { viewId } = interaction; - this.viewInstances.get(viewId)?.close(); - + this.disposeView(viewId); break; } @@ -175,9 +211,12 @@ export class ActionManager implements IActionManager { case 'contextual_bar.close': { const { view } = interaction; - this.viewInstances.get(view.id)?.close(); + this.disposeView(view.id); break; } + + default: + exhaustiveCheck(interaction); } return interaction.type; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 06b91e424835..192b3a76a315 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.7.2" + "version": "6.8.0-rc.2" } diff --git a/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts new file mode 100644 index 000000000000..fa300cb37ee2 --- /dev/null +++ b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts @@ -0,0 +1,6 @@ +import { settings } from '../../../settings/server'; + +export const isSMTPConfigured = (): boolean => { + const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); + return Boolean(settings.get('SMTP_Host')) || isMailURLSet; +}; diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts index f3cb9d2f967e..fb55d478b6f5 100644 --- a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts @@ -13,6 +13,10 @@ export const buildVersionUpdateMessage = async ( infoUrl: string; }[] = [], ) => { + if (process.env.TEST_MODE) { + return; + } + const lastCheckedVersion = settings.get('Update_LatestAvailableVersion'); if (!lastCheckedVersion) { diff --git a/apps/meteor/client/components/AutoCompleteDepartment.tsx b/apps/meteor/client/components/AutoCompleteDepartment.tsx index 4688899890fb..6217f1d99610 100644 --- a/apps/meteor/client/components/AutoCompleteDepartment.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartment.tsx @@ -51,8 +51,8 @@ const AutoCompleteDepartment = ({ return (