diff --git a/.changeset/chilly-sheep-cover.md b/.changeset/chilly-sheep-cover.md new file mode 100644 index 0000000000000..e4cb096bd061a --- /dev/null +++ b/.changeset/chilly-sheep-cover.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a new endpoint `rooms.hide` to hide rooms of any type when provided with the room's ID diff --git a/.changeset/cool-coins-agree.md b/.changeset/cool-coins-agree.md new file mode 100644 index 0000000000000..d89d07c1fa384 --- /dev/null +++ b/.changeset/cool-coins-agree.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes a bug that caused routing algorithms to ignore the `Livechat_enabled_when_agent_idle` setting, effectively ignoring idle users from being assigned to inquiries. diff --git a/.changeset/dull-hounds-agree.md b/.changeset/dull-hounds-agree.md new file mode 100644 index 0000000000000..543fbbeb67542 --- /dev/null +++ b/.changeset/dull-hounds-agree.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/models": minor +"@rocket.chat/rest-typings": minor +--- + +Allows users to filter by multiple departments & by livechat units on `livechat/rooms` endpoint. diff --git a/.changeset/fast-starfishes-perform.md b/.changeset/fast-starfishes-perform.md new file mode 100644 index 0000000000000..d799f448ef9d4 --- /dev/null +++ b/.changeset/fast-starfishes-perform.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes broken link and improves messaging for invalid apps banner. diff --git a/.changeset/five-cherries-hang.md b/.changeset/five-cherries-hang.md new file mode 100644 index 0000000000000..bc33eb7cf309b --- /dev/null +++ b/.changeset/five-cherries-hang.md @@ -0,0 +1,20 @@ +--- +'@rocket.chat/omnichannel-transcript': patch +'@rocket.chat/authorization-service': patch +'@rocket.chat/stream-hub-service': patch +'@rocket.chat/network-broker': patch +'@rocket.chat/presence-service': patch +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/account-service': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/ddp-streamer': patch +'@rocket.chat/queue-worker': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/apps': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes behavior of app updates that would save undesired field changes to documents diff --git a/.changeset/five-wolves-destroy.md b/.changeset/five-wolves-destroy.md new file mode 100644 index 0000000000000..051b77c13b710 --- /dev/null +++ b/.changeset/five-wolves-destroy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a UI issue where enabling/disabling TOTP two factor authentication didn't update in real-time. diff --git a/.changeset/flat-balloons-draw.md b/.changeset/flat-balloons-draw.md new file mode 100644 index 0000000000000..c9035b7bf2e2b --- /dev/null +++ b/.changeset/flat-balloons-draw.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where backup codes modal is not opening when regenerating codes diff --git a/.changeset/funny-turtles-hunt.md b/.changeset/funny-turtles-hunt.md new file mode 100644 index 0000000000000..982dc542325ff --- /dev/null +++ b/.changeset/funny-turtles-hunt.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds a new setting that if enabled, will not count bot messages in the average response time metrics diff --git a/.changeset/green-rules-cover.md b/.changeset/green-rules-cover.md new file mode 100644 index 0000000000000..0501761799753 --- /dev/null +++ b/.changeset/green-rules-cover.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Improves color contrast in image gallery icon buttons to meet WCAG compliance. diff --git a/.changeset/happy-nails-fry.md b/.changeset/happy-nails-fry.md new file mode 100644 index 0000000000000..ebc17ea2dfe8b --- /dev/null +++ b/.changeset/happy-nails-fry.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/apps": minor +--- + +Adds a new IPostSystemMessageSent event, that is triggered whenever a new System Message is sent diff --git a/.changeset/honest-experts-mate.md b/.changeset/honest-experts-mate.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/honest-experts-mate.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/itchy-teachers-jam.md b/.changeset/itchy-teachers-jam.md new file mode 100644 index 0000000000000..b6ddf809c621f --- /dev/null +++ b/.changeset/itchy-teachers-jam.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where menus inside sidebar keeps opened even if the sidebar is collapsed diff --git a/.changeset/lazy-cheetahs-learn.md b/.changeset/lazy-cheetahs-learn.md new file mode 100644 index 0000000000000..64af59a321fb4 --- /dev/null +++ b/.changeset/lazy-cheetahs-learn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes app event `IPostLivechatAgentAssigned` receiving a room object previous to the assignment of agent, causing `room.servedBy` to be undefined on apps diff --git a/.changeset/lemon-lions-learn.md b/.changeset/lemon-lions-learn.md new file mode 100644 index 0000000000000..8ed1d10bccf0a --- /dev/null +++ b/.changeset/lemon-lions-learn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes the possibility of see new messages without being subscribed to a public channel. diff --git a/.changeset/light-yaks-drive.md b/.changeset/light-yaks-drive.md new file mode 100644 index 0000000000000..b36dcdeb02bb1 --- /dev/null +++ b/.changeset/light-yaks-drive.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Implements a modal to let users know about VoIP calls in direct messages and missing configurations. diff --git a/.changeset/lucky-birds-lie.md b/.changeset/lucky-birds-lie.md new file mode 100644 index 0000000000000..a1d7a0db6a0ee --- /dev/null +++ b/.changeset/lucky-birds-lie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue which caused some weird scrolling in rooms when using secondary sidepanel navigation. diff --git a/.changeset/mean-elephants-boil.md b/.changeset/mean-elephants-boil.md new file mode 100644 index 0000000000000..fa091f08a8db9 --- /dev/null +++ b/.changeset/mean-elephants-boil.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue that would cause marketplace apps to become invalid installations after an update diff --git a/.changeset/mean-numbers-chew.md b/.changeset/mean-numbers-chew.md new file mode 100644 index 0000000000000..b4e3d6acfe39f --- /dev/null +++ b/.changeset/mean-numbers-chew.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": patch +"@rocket.chat/rest-typings": patch +"@rocket.chat/ui-voip": patch +--- + +Enables control of video conference ringing and dialing sounds through the call ringer volume user preference, preventing video conf calls from always playing at maximum volume. diff --git a/.changeset/nervous-eels-march.md b/.changeset/nervous-eels-march.md new file mode 100644 index 0000000000000..efd080a4c6cc4 --- /dev/null +++ b/.changeset/nervous-eels-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Adds unicode character support for default avatars diff --git a/.changeset/new-ears-check.md b/.changeset/new-ears-check.md new file mode 100644 index 0000000000000..1fe714395d59b --- /dev/null +++ b/.changeset/new-ears-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a UI issue where enabling/disabling email two factor authentication didn't update in real-time. diff --git a/.changeset/ninety-apes-poke.md b/.changeset/ninety-apes-poke.md new file mode 100644 index 0000000000000..bab3e9154a9e9 --- /dev/null +++ b/.changeset/ninety-apes-poke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Enhances message sorting in the `im.messages` and `dm.messages` endpoints by enabling support for multi-parameter sorting. diff --git a/.changeset/perfect-trees-rescue.md b/.changeset/perfect-trees-rescue.md new file mode 100644 index 0000000000000..1e20beebbc32d --- /dev/null +++ b/.changeset/perfect-trees-rescue.md @@ -0,0 +1,13 @@ +--- +'rocketchat-services': patch +'@rocket.chat/omnichannel-transcript': patch +'@rocket.chat/authorization-service': patch +'@rocket.chat/stream-hub-service': patch +'@rocket.chat/presence-service': patch +'@rocket.chat/account-service': patch +'@rocket.chat/ddp-streamer': patch +'@rocket.chat/queue-worker': patch +'@rocket.chat/meteor': patch +--- + +Bump meteor to 3.1.2 and Node version to 20.13.1 diff --git a/.changeset/popular-boats-poke.md b/.changeset/popular-boats-poke.md new file mode 100644 index 0000000000000..ef9289c0877aa --- /dev/null +++ b/.changeset/popular-boats-poke.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/omnichannel-services": patch +"@rocket.chat/pdf-worker": patch +--- + +Fixes omnichannel transcript filename breaking download links diff --git a/.changeset/popular-mugs-try.md b/.changeset/popular-mugs-try.md new file mode 100644 index 0000000000000..030422a3a76a1 --- /dev/null +++ b/.changeset/popular-mugs-try.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the room converter would throw if the room was an omnichannel room that had been closed by the visitor diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..8174c50eb840e --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,124 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "7.4.0-develop", + "rocketchat-services": "2.0.4", + "@rocket.chat/uikit-playground": "0.6.4", + "@rocket.chat/account-service": "0.4.13", + "@rocket.chat/authorization-service": "0.4.13", + "@rocket.chat/ddp-streamer": "0.3.13", + "@rocket.chat/omnichannel-transcript": "0.4.13", + "@rocket.chat/presence-service": "0.4.13", + "@rocket.chat/queue-worker": "0.4.13", + "@rocket.chat/stream-hub-service": "0.4.13", + "@rocket.chat/license": "1.0.4", + "@rocket.chat/network-broker": "0.1.5", + "@rocket.chat/omnichannel-services": "0.3.10", + "@rocket.chat/pdf-worker": "0.2.10", + "@rocket.chat/presence": "0.2.13", + "@rocket.chat/ui-theming": "0.4.2", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.13", + "@rocket.chat/apps": "0.2.4", + "@rocket.chat/apps-engine": "1.48.2", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.2", + "@rocket.chat/core-services": "0.7.5", + "@rocket.chat/core-typings": "7.4.0-develop", + "@rocket.chat/cron": "0.1.13", + "@rocket.chat/ddp-client": "0.3.13", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/freeswitch": "1.2.0", + "@rocket.chat/fuselage-ui-kit": "15.0.0", + "@rocket.chat/gazzodown": "15.0.0", + "@rocket.chat/i18n": "1.3.0", + "@rocket.chat/instance-status": "0.1.13", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.22.0", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.31", + "@rocket.chat/mock-providers": "0.1.7", + "@rocket.chat/model-typings": "1.3.0", + "@rocket.chat/models": "1.2.0", + "@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.27", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "7.4.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.12", + "@rocket.chat/tools": "0.2.2", + "@rocket.chat/tracing": "0.0.1", + "@rocket.chat/ui-avatar": "11.0.0", + "@rocket.chat/ui-client": "15.0.0", + "@rocket.chat/ui-composer": "0.5.1", + "@rocket.chat/ui-contexts": "15.0.0", + "@rocket.chat/ui-kit": "0.37.0", + "@rocket.chat/ui-video-conf": "15.0.0", + "@rocket.chat/ui-voip": "5.0.0", + "@rocket.chat/web-ui-registration": "15.0.0" + }, + "changesets": [ + "brave-ties-shout", + "chilled-shirts-smile", + "chilly-sheep-cover", + "cool-coins-agree", + "cuddly-garlics-cover", + "dry-jeans-thank", + "dull-hounds-agree", + "fast-starfishes-perform", + "five-cherries-hang", + "five-wolves-destroy", + "funny-ears-kick", + "funny-turtles-hunt", + "green-rules-cover", + "happy-nails-fry", + "honest-experts-mate", + "kind-geckos-heal", + "lazy-cheetahs-learn", + "lemon-lions-learn", + "light-yaks-drive", + "lucky-birds-lie", + "many-badgers-jam", + "mean-elephants-boil", + "mean-numbers-chew", + "new-ears-check", + "ninety-apes-poke", + "olive-stingrays-share", + "perfect-trees-rescue", + "popular-boats-poke", + "popular-mugs-try", + "popular-stingrays-trade", + "proud-mayflies-invite", + "purple-laws-joke", + "rotten-peaches-cry", + "rude-adults-shout", + "rude-llamas-pay", + "seven-otters-worry", + "silly-emus-remain", + "silly-shiny-kiwis", + "six-deers-dress", + "slow-flies-hear", + "small-waves-press", + "sour-rabbits-flow", + "stupid-rats-work", + "sweet-timers-divide", + "thick-boats-shake", + "three-insects-roll", + "tough-eggs-camp", + "twenty-camels-worry", + "violet-bikes-brake", + "weak-kangaroos-admire", + "yellow-cars-change", + "yellow-houses-beg" + ] +} diff --git a/.changeset/purple-laws-joke.md b/.changeset/purple-laws-joke.md new file mode 100644 index 0000000000000..681e6aa43cf1f --- /dev/null +++ b/.changeset/purple-laws-joke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where non-Latin highlights were inconsistently applied, ensuring non-Latin characters are reliably detected and highlighted. diff --git a/.changeset/rotten-peaches-cry.md b/.changeset/rotten-peaches-cry.md new file mode 100644 index 0000000000000..3a6ab5314a4a5 --- /dev/null +++ b/.changeset/rotten-peaches-cry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Removes the Leader role from the room header. Leaders are now displayed in their respective group within the room's members list. diff --git a/.changeset/rude-llamas-pay.md b/.changeset/rude-llamas-pay.md new file mode 100644 index 0000000000000..c58da7f22049f --- /dev/null +++ b/.changeset/rude-llamas-pay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes Livechat conversation not closing in a few scenarios due to cross-tab interference diff --git a/.changeset/six-deers-dress.md b/.changeset/six-deers-dress.md new file mode 100644 index 0000000000000..84842adb03151 --- /dev/null +++ b/.changeset/six-deers-dress.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes an issue with embedded layout rooms displaying as if the user is not part of the room diff --git a/.changeset/sour-rabbits-flow.md b/.changeset/sour-rabbits-flow.md new file mode 100644 index 0000000000000..05242a97ecd9a --- /dev/null +++ b/.changeset/sour-rabbits-flow.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Validates duplicated email and phone number when creating or editing omnichannel contacts. diff --git a/.changeset/sweet-timers-divide.md b/.changeset/sweet-timers-divide.md new file mode 100644 index 0000000000000..8d5e41b5522b8 --- /dev/null +++ b/.changeset/sweet-timers-divide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Switches from GCM (unsupported) to FCM as the default push notification service for custom mobile apps diff --git a/.changeset/tough-eggs-camp.md b/.changeset/tough-eggs-camp.md new file mode 100644 index 0000000000000..10616da2b6675 --- /dev/null +++ b/.changeset/tough-eggs-camp.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +--- + +Enables specifying hidden settings that are enabled to be accessed through the apps-engine in the permission list. diff --git a/.changeset/two-flowers-bake.md b/.changeset/two-flowers-bake.md new file mode 100644 index 0000000000000..3e1851309cdbe --- /dev/null +++ b/.changeset/two-flowers-bake.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes a very rare issue where switching from a DM to a regular room would show an error page diff --git a/.changeset/weak-kangaroos-admire.md b/.changeset/weak-kangaroos-admire.md new file mode 100644 index 0000000000000..93528c33ef30d --- /dev/null +++ b/.changeset/weak-kangaroos-admire.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds the Leader group to rooms' members list for better role visibility and consistency. diff --git a/.changeset/yellow-houses-beg.md b/.changeset/yellow-houses-beg.md new file mode 100644 index 0000000000000..46fd6a14603c8 --- /dev/null +++ b/.changeset/yellow-houses-beg.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue which was preventing admins in enterprise workspaces from updating some premium layout settings. diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index 666b91bf3c817..18bbb1abce31a 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -71,11 +71,12 @@ } }, "node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", + "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.0.0", + "@octokit/types": "^13.6.2", "universal-user-agent": "^7.0.2" }, "engines": { @@ -96,16 +97,18 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", - "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz", + "integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.5.0" + "@octokit/types": "^13.7.0" }, "engines": { "node": ">= 18" @@ -140,13 +143,15 @@ } }, "node_modules/@octokit/request": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", - "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz", + "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^10.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.1.0", + "@octokit/endpoint": "^10.1.3", + "@octokit/request-error": "^6.1.7", + "@octokit/types": "^13.6.2", + "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" }, "engines": { @@ -154,11 +159,12 @@ } }, "node_modules/@octokit/request-error": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", - "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", + "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.0.0" + "@octokit/types": "^13.6.2" }, "engines": { "node": ">= 18" @@ -179,11 +185,12 @@ } }, "node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@octokit/openapi-types": "^23.0.1" } }, "node_modules/@xmldom/xmldom": { @@ -255,6 +262,22 @@ "node": ">=0.3.1" } }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 3d4940e5cb0d4..7f0dbd485aabd 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -47,7 +47,7 @@ jobs: # docker rmi $(docker image ls -aq) # df -h - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Restore turbo build uses: actions/download-artifact@v4 diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index 6a343dd8476b1..97c59ac5d915a 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 6a4aa84c8da45..6c609f77dfdd6 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -130,7 +130,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Restore turbo build uses: actions/download-artifact@v4 diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 3c02c933ddb40..9705ee17e2f8b 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -45,7 +45,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Restore turbo build uses: actions/download-artifact@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2697b0bfc2796..4124565c4a26b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,7 +173,7 @@ jobs: restore-keys: | vite-local-cache-${{ runner.os }}- - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Build Rocket.Chat Packages run: yarn build @@ -192,14 +192,14 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 if: github.event.action != 'closed' - name: Setup NodeJS uses: ./.github/actions/setup-node if: github.event.action != 'closed' with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 73774c18cee25..688a02bc1caa5 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -34,13 +34,13 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Build packages run: yarn build diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 0163404fb5e0a..4f5c089f8ffca 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -21,13 +21,13 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Build packages run: yarn build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e45b88d5bca97..046bc470ba04d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -24,13 +24,13 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Build packages run: yarn build diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 640574d3eca39..a1751b9cb6f36 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -15,13 +15,13 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 22.11.0 + node-version: 22.13.1 deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@v1.5 + - uses: rharkor/caching-for-turbo@v1.6 - name: Build packages run: yarn build diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 1972e8181c713..0b3fb34c4e6fd 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -19,7 +19,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4.1.0 with: - node-version: '22.11.0' + node-version: '22.13.1' - name: Install dependencies run: | diff --git a/README.md b/README.md index 24ce42485bdeb..1dd982e9bcfad 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We are the ultimate **Free Open Source Solution** for team communications, enabl Every day, tens of millions of users in over 150 countries and in organizations such as Deutsche Bahn, The US Navy, and Credit Suisse trust Rocket.Chat to keep their communications completely private and secure. -# 🚀 Product Offerings - Self Hosted and Cloud +# 🚀 Product Offerings - Self-hosted and Cloud Rocket.Chat has four key product offerings: @@ -35,88 +35,54 @@ Rocket.Chat has four key product offerings: -# ☁️ Cloud Hosted Rocket.Chat +# ☁️ Cloud-hosted Rocket.Chat -Send your first message in minutes. - -Free for 30 days. Afterward, choose between continuing to host on our secure cloud or migrating to your private cloud, data center, or even air-gapped environment. - -[Start your cloud hosted trial now](https://rocket.chat/trial-saas) +Rocket.Chat has flexible hosting options that adapt to your infrastructure needs. +For more information please [follow this link](https://www.rocket.chat/hosting) # 📖 Docs for Developers, Admins and Users -Please make sure to visit our [Docs](https://docs.rocket.chat/) and [Developer Docs](https://developer.rocket.chat/docs) before sending questions. - -# 🛠️ Local development - -## Prerequisites - -You can follow these instructions to setup a dev environment: - -- Install **Node 22.x (LTS)** either [manually](https://nodejs.org/dist/latest-v22.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) -- Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html -- Install **yarn**: https://yarnpkg.com/getting-started/install -- Install **Deno 1.x**: https://docs.deno.com/runtime/fundamentals/installation/ -- Clone this repo: `git clone https://github.com/RocketChat/Rocket.Chat.git` -- Run `yarn` to install dependencies - -**Starting Rocket.Chat:** - -```bash -yarn dev # run all packages -``` -OR -```bash -yarn dsv # run only meteor (front and back) with pre-built packages -``` - -After initialized, you can access the server at http://localhost:3000 -More details at: [Developer Docs](https://developer.rocket.chat/v1/docs/server-environment-setup) -PS: For Windows you MUST use WSL2 and have +12Gb RAM +Visit our official [User Documentation](https://docs.rocket.chat/) and [Developer Docs](https://developer.rocket.chat/docs) before sending questions. +# 🛠️ Local Development -# Gitpod Setup +You can set up a Rocket.Chat server development environment by following the guide below for your operating system: -1. Click the button below to open this project in Gitpod. -2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed. +- [Linux](https://developer.rocket.chat/docs/linux): See how to set up a Rocket.Chat server development environment on any Linux distribution. -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/RocketChat/Rocket.Chat) +- [Mac OSX](https://developer.rocket.chat/docs/mac-osx): Learn how to set up a Rocket.Chat server development environment on Mac. -**Starting Rocket.Chat in microservices mode:** +- [Windows 10](https://developer.rocket.chat/docs/windows-10): Set up a Rocket.Chat server development environment on Windows. -```bash -yarn turbo run ms -``` +- [Gitpod](https://developer.rocket.chat/docs/gitpod): Use the online development environment pre-configuration to develop Rocket.Chat server. -After initialized, you can access the server at http://localhost:4000 +> Visit the [Rocket.Chat Environment Setup guide](https://developer.rocket.chat/docs/server-environment-setup) to learn more. -> ⚠️ Check more detailed information in the [Rocket.Chat Environment Setup](https://developer.rocket.chat/docs/server-environment-setup) guide -# 💻 Installation +# 💻 Deploy Rocket.Chat -Please see the [requirements documentation](https://docs.rocket.chat/deploy/installing-client-apps/minimum-requirements-for-using-rocket.chat) for system requirements and more information about supported operating systems. -Please refer to [Install Rocket.Chat](https://rocket.chat/install) to install your Rocket.Chat instance. +Refer to the [System requirements documentation](https://docs.rocket.chat/docs/system-requirements) for required hardware and software specifications. For detailed instructions on deploying your Rocket.Chat workspace, visit [Deploy Rocket.Chat](https://rocket.chat/install). -# 📱 Mobile Apps +# 📱 Mobile apps -In addition to the web interface, you can also download Rocket.Chat clients for: +In addition to the web app, you can also download Rocket.Chat clients for: [![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android) -You can also contribute to the Mobile open source code in [Rocket.Chat.ReactNative](https://github.com/RocketChat/Rocket.Chat.ReactNative) and check it out its [documentation](https://developer.rocket.chat/mobile-app/mobile-app-environment-setup) +You can also contribute to the mobile open source code in [Rocket.Chat.ReactNative](https://github.com/RocketChat/Rocket.Chat.ReactNative) and check it out its [documentation](https://developer.rocket.chat/docs/mobile-app). # 🧩 Apps Engine for Rocket.Chat -You can develop your own app that can be integrated with Rocket.Chat. We provide an [Open Source Apps Engine framework](https://developer.rocket.chat/apps-engine/getting-started) increasing the world of possibilities of integrations around the Rocket.Chat ecosystem +You can develop your own app that can be integrated with Rocket.Chat. We provide an [Open Source Apps-Engine framework](https://developer.rocket.chat/apps-engine/getting-started) which expands the integration possibilities within the Rocket.Chat ecosystem. # 📚 Learn More -- [Product Documentation](https://docs.rocket.chat) -- [Developer Documentation](https://developer.rocket.chat) -- [API Documentation](https://developer.rocket.chat/reference/api) -- [Apps Engine Development](https://developer.rocket.chat/apps-engine/rocket.chat-apps-and-apps-engine) +- [User documentation](https://docs.rocket.chat) +- [Developer documentation](https://developer.rocket.chat) +- [API documentation](https://developer.rocket.chat/reference/api) +- [Apps-Engine development](https://developer.rocket.chat/apps-engine/rocket.chat-apps-and-apps-engine) - [See who's using Rocket.Chat](https://www.rocket.chat/customers) # 🆕 Feature Request @@ -131,14 +97,14 @@ Join [#support](https://open.rocket.chat/channel/support) and [#general](https:/ # 👥 Contributions -Rocket.Chat is an open source project and we are very happy to accept community contributions. Please refer to the [How can I help?](https://developer.rocket.chat/contribute-to-rocket.chat/ways-to-contribute) page for more details. +Rocket.Chat is an open source project and we are very happy to accept community contributions. Refer to the [Modes of contribution guide](https://developer.rocket.chat/contribute-to-rocket.chat/ways-to-contribute) for more details. -## 💼 Become a Rocketeer +# 💼 Become a Rocketeer -We're hiring developers, support people, and product managers all the time. Please check our [jobs page](https://rocket.chat/jobs). +We're hiring developers, support people, and product managers all the time. Check out our [jobs page](https://rocket.chat/jobs). -## 🗞️ Get the Latest News +# 🗞️ Get the Latest News - [Blog](https://rocket.chat/blog) - [Twitter](https://twitter.com/RocketChat) @@ -146,6 +112,6 @@ We're hiring developers, support people, and product managers all the time. Plea - [LinkedIn](https://www.linkedin.com/company/rocket-chat) - [Youtube](https://www.youtube.com/channel/UCin9nv7mUjoqrRiwrzS5UVQ) -## 🗒️ Credits +# 🗒️ Credits - Emoji provided graciously by [JoyPixels](https://www.joypixels.com). diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index b0880bd475392..ec307ecdce7d8 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -3,7 +3,7 @@ to: ee/apps/<%= name %>/src/service.ts --- import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { registerServiceModels } from '@rocket.chat/models'; -import { broker } from '@rocket.chat/network-broker'; +import { startBroker } from '@rocket.chat/network-broker'; import { startTracing } from '@rocket.chat/tracing'; import polka from 'polka'; @@ -16,7 +16,7 @@ const PORT = process.env.PORT || <%= h.random() %>; registerServiceModels(db, await getTrashCollection()); - api.setBroker(broker); + api.setBroker(startBroker()); // need to import service after models are registered const { <%= h.changeCase.pascalCase(name) %> } = await import('./<%= h.changeCase.pascalCase(name) %>'); diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile index faa3c6138ca90..2d85a15eca878 100644 --- a/apps/meteor/.docker-mongo/Dockerfile +++ b/apps/meteor/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.11.0-bullseye-slim +FROM node:22.13.1-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index e4f8b11cb96b1..e098d21722a5d 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM node:22.11.0-alpine3.20 +FROM node:22.13.1-alpine3.20 LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.debian b/apps/meteor/.docker/Dockerfile.debian index 7b1094b46508e..9a4012d1e5b39 100644 --- a/apps/meteor/.docker/Dockerfile.debian +++ b/apps/meteor/.docker/Dockerfile.debian @@ -2,7 +2,7 @@ ARG DENO_VERSION="1.37.1" FROM denoland/deno:bin-${DENO_VERSION} as deno -FROM node:22.11.0-bullseye-slim +FROM node:22.13.1-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index efc4541f666d9..f7b92d4cf9a85 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -13,29 +13,29 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@3.0.3 +accounts-base@3.0.4 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 -accounts-oauth@1.4.5 +accounts-oauth@1.4.6 accounts-password@3.0.3 accounts-twitter@1.5.2 google-oauth@1.4.5 -oauth@3.0.0 +oauth@3.0.1 oauth2@1.3.3 check@1.4.4 ddp-rate-limiter@1.2.2 rate-limit@1.1.2 -email@3.1.1 +email@3.1.2 meteor-base@1.5.2 ddp-common@1.4.4 -webapp@2.0.4 +webapp@2.0.5 -mongo@2.0.3 +mongo@2.1.0 reload@1.3.2 service-configuration@1.3.5 @@ -45,7 +45,6 @@ shell-server@0.6.1 dispatch:run-as-user ostrio:cookies -kadira:flow-router meteorhacks:inject-initial @@ -58,7 +57,7 @@ tracker@1.3.4 reactive-dict@1.3.2 reactive-var@1.0.13 -babel-compiler@7.11.2 +babel-compiler@7.11.3 standard-minifier-css@1.9.3 dynamic-import@0.7.4 ecmascript@0.16.10 @@ -70,3 +69,4 @@ autoupdate@2.0.0 zodern:types zodern:standard-minifier-js +ostrio:flow-router-extra diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 8d20e1a2d3a87..5f22892744ba2 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.1 +METEOR@3.1.2 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index e3d5dd574f1e3..979496429ffe3 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,14 +1,14 @@ -accounts-base@3.0.3 +accounts-base@3.0.4 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 -accounts-oauth@1.4.5 +accounts-oauth@1.4.6 accounts-password@3.0.3 accounts-twitter@1.5.2 -allow-deny@2.0.0 +allow-deny@2.1.0 autoupdate@2.0.0 -babel-compiler@7.11.2 +babel-compiler@7.11.3 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 @@ -17,10 +17,10 @@ callback-hook@1.6.0 check@1.4.4 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.0.3 +ddp-client@3.1.0 ddp-common@1.4.4 ddp-rate-limiter@1.2.2 -ddp-server@3.0.3 +ddp-server@3.1.0 diff-sequence@1.1.3 dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 @@ -29,9 +29,9 @@ ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 -email@3.1.1 +email@3.1.2 es5-shim@4.8.1 -facebook-oauth@1.11.4 +facebook-oauth@1.11.5 facts-base@1.0.2 fetch@0.1.5 geojson-utils@1.0.12 @@ -41,28 +41,28 @@ hot-code-push@1.0.5 http@3.0.0 id-map@1.2.0 inter-process-messaging@0.1.2 -kadira:flow-router@2.12.1 localstorage@1.2.1 logging@1.3.5 -meteor@2.0.2 +meteor@2.1.0 meteor-base@1.5.2 meteor-developer-oauth@1.3.3 meteorhacks:inject-initial@1.0.5 minifier-css@2.0.0 minimongo@2.0.2 -modern-browsers@0.1.11 +modern-browsers@0.2.0 modules@0.20.3 modules-runtime@0.13.2 -mongo@2.0.3 +mongo@2.1.0 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 -npm-mongo@6.10.0 -oauth@3.0.0 +npm-mongo@6.10.2 +oauth@3.0.1 oauth1@1.5.2 oauth2@1.3.3 ordered-dict@1.2.0 ostrio:cookies@2.7.2 +ostrio:flow-router-extra@3.11.0 promise@1.0.0 random@1.2.2 rate-limit@1.1.2 @@ -82,15 +82,15 @@ service-configuration@1.3.5 session@1.2.2 sha@1.0.10 shell-server@0.6.1 -socket-stream-client@0.5.3 +socket-stream-client@0.6.0 standard-minifier-css@1.9.3 tracker@1.3.4 twitter-oauth@1.3.4 typescript@5.6.3 underscore@1.6.4 url@1.3.5 -webapp@2.0.4 +webapp@2.0.5 webapp-hashing@1.1.2 zodern:caching-minifier@0.5.0 -zodern:standard-minifier-js@5.2.0 -zodern:types@1.0.10 +zodern:standard-minifier-js@5.3.1 +zodern:types@1.0.13 diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 94543501416c6..f08e87353e48f 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,227 @@ # @rocket.chat/meteor +## 7.4.0-rc.0 + +### Minor Changes + +- ([#34208](https://github.com/RocketChat/Rocket.Chat/pull/34208)) Adds a new endpoint `rooms.hide` to hide rooms of any type when provided with the room's ID + +- ([#35147](https://github.com/RocketChat/Rocket.Chat/pull/35147)) Allows users to filter by multiple departments & by livechat units on `livechat/rooms` endpoint. + +- ([#34274](https://github.com/RocketChat/Rocket.Chat/pull/34274)) Adds a new setting that if enabled, will not count bot messages in the average response time metrics + +- ([#35177](https://github.com/RocketChat/Rocket.Chat/pull/35177)) Adds a new IPostSystemMessageSent event, that is triggered whenever a new System Message is sent + +- ([#34957](https://github.com/RocketChat/Rocket.Chat/pull/34957)) Implements a modal to let users know about VoIP calls in direct messages and missing configurations. + +- ([#34958](https://github.com/RocketChat/Rocket.Chat/pull/34958)) Makes Omnichannel converstion start process transactional. + +- ([#34926](https://github.com/RocketChat/Rocket.Chat/pull/34926)) Enables control of video conference ringing and dialing sounds through the call ringer volume user preference, preventing video conf calls from always playing at maximum volume. + +- ([#35260](https://github.com/RocketChat/Rocket.Chat/pull/35260)) Enhances message sorting in the `im.messages` and `dm.messages` endpoints by enabling support for multi-parameter sorting. + +- ([#34939](https://github.com/RocketChat/Rocket.Chat/pull/34939)) Improves the search of permissions + +- ([#35113](https://github.com/RocketChat/Rocket.Chat/pull/35113)) Includes attachments metadata in JSON export if type is file when exporting messages + +- ([#33885](https://github.com/RocketChat/Rocket.Chat/pull/33885)) Fixes an issue where tokens generated while using the legacy notification provider would never be removed from the database. + +- ([#33816](https://github.com/RocketChat/Rocket.Chat/pull/33816) by [@matheusbsilva137](https://github.com/matheusbsilva137)) Replaces Livechat Visitors by Contacts on workspaces' MAC count. + This allows a more accurate and potentially smaller MAC count in case Contact Identification is enabled, since multiple visitors may be associated to the same contact. +- ([#35038](https://github.com/RocketChat/Rocket.Chat/pull/35038)) Adds "DOMPurify" and "he" to sanitize ECDH and Livechat errors + +- ([#35013](https://github.com/RocketChat/Rocket.Chat/pull/35013)) Adds a filter option to include or exclude threads in the Apps Engine room read/unread messages bridge, enhancing flexibility in message retrieval. + +- ([#35199](https://github.com/RocketChat/Rocket.Chat/pull/35199)) Enables specifying hidden settings that are enabled to be accessed through the apps-engine in the permission list. + +- ([#35089](https://github.com/RocketChat/Rocket.Chat/pull/35089)) Adds wrapExceptions to handle an unhandled promise rejection when adding and/or updating OAuth apps + +- ([#35208](https://github.com/RocketChat/Rocket.Chat/pull/35208)) Adds the Leader group to rooms' members list for better role visibility and consistency. + +### Patch Changes + +- ([#35189](https://github.com/RocketChat/Rocket.Chat/pull/35189)) fixes toast with empty error messages when a private app installation fails + +- ([#34980](https://github.com/RocketChat/Rocket.Chat/pull/34980)) Implements removal of OTR (off-the-record) conversation messsages just after OTR session ends. + +- ([#35029](https://github.com/RocketChat/Rocket.Chat/pull/35029)) Fixes a bug that caused routing algorithms to ignore the `Livechat_enabled_when_agent_idle` setting, effectively ignoring idle users from being assigned to inquiries. + +- ([#35079](https://github.com/RocketChat/Rocket.Chat/pull/35079)) Fixes unused `i18nTitle` provided by the app in message composer popup previewer + +- ([#35044](https://github.com/RocketChat/Rocket.Chat/pull/35044)) Fixes an issue that allowed departments to be removed via API even with setting `Omnichannel_enable_department_removal` disabled + +- ([#35132](https://github.com/RocketChat/Rocket.Chat/pull/35132)) Fixes broken link and improves messaging for invalid apps banner. + +- ([#35120](https://github.com/RocketChat/Rocket.Chat/pull/35120)) Fixes behavior of app updates that would save undesired field changes to documents + +- ([#35188](https://github.com/RocketChat/Rocket.Chat/pull/35188)) Fixes a UI issue where enabling/disabling TOTP two factor authentication didn't update in real-time. + +- ([#35082](https://github.com/RocketChat/Rocket.Chat/pull/35082)) Fixes a rerender on each sidebar item click + +- ([#35233](https://github.com/RocketChat/Rocket.Chat/pull/35233)) Improves color contrast in image gallery icon buttons to meet WCAG compliance. + +- ([#35261](https://github.com/RocketChat/Rocket.Chat/pull/35261)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#35100](https://github.com/RocketChat/Rocket.Chat/pull/35100)) Fixes incorrect start date on omnichannel reports + +- ([#35265](https://github.com/RocketChat/Rocket.Chat/pull/35265)) Fixes app event `IPostLivechatAgentAssigned` receiving a room object previous to the assignment of agent, causing `room.servedBy` to be undefined on apps + +- ([#35194](https://github.com/RocketChat/Rocket.Chat/pull/35194)) fixes the possibility of see new messages without being subscribed to a public channel. + +- ([#35121](https://github.com/RocketChat/Rocket.Chat/pull/35121)) Fixes an issue which caused some weird scrolling in rooms when using secondary sidepanel navigation. + +- ([#35170](https://github.com/RocketChat/Rocket.Chat/pull/35170)) Fixes an issue that would cause marketplace apps to become invalid installations after an update + +- ([#35140](https://github.com/RocketChat/Rocket.Chat/pull/35140)) Fixes a UI issue where enabling/disabling email two factor authentication didn't update in real-time. + +- ([#35181](https://github.com/RocketChat/Rocket.Chat/pull/35181)) Bump meteor to 3.1.2 and Node version to 20.13.1 + +- ([#35107](https://github.com/RocketChat/Rocket.Chat/pull/35107)) Fixes an issue where the room converter would throw if the room was an omnichannel room that had been closed by the visitor + +- ([#35124](https://github.com/RocketChat/Rocket.Chat/pull/35124)) Fixes an issue where non-Latin highlights were inconsistently applied, ensuring non-Latin characters are reliably detected and highlighted. + +- ([#35208](https://github.com/RocketChat/Rocket.Chat/pull/35208)) Removes the Leader role from the room header. Leaders are now displayed in their respective group within the room's members list. + +- ([#34857](https://github.com/RocketChat/Rocket.Chat/pull/34857) by [@AyushKumar123456789](https://github.com/AyushKumar123456789)) Fixes an issue where custom status' values in the custom status page table were not being properly translated + +- ([#34987](https://github.com/RocketChat/Rocket.Chat/pull/34987)) Fixes a behavior in Omnichannel that was causing bot agents to be waiting in the queue, when they should always skip it. + +- ([#34988](https://github.com/RocketChat/Rocket.Chat/pull/34988)) Fixes `channels.list` endpoint from rejecting pagination parameters + +- ([#35204](https://github.com/RocketChat/Rocket.Chat/pull/35204)) fixes an issue with embedded layout rooms displaying as if the user is not part of the room + +- ([#35032](https://github.com/RocketChat/Rocket.Chat/pull/35032)) Validates duplicated email and phone number when creating or editing omnichannel contacts. + +- ([#35143](https://github.com/RocketChat/Rocket.Chat/pull/35143)) Switches from GCM (unsupported) to FCM as the default push notification service for custom mobile apps + +- ([#35167](https://github.com/RocketChat/Rocket.Chat/pull/35167)) Fixes an issue where assign extension button were missing the proper margin + +- ([#35028](https://github.com/RocketChat/Rocket.Chat/pull/35028)) Fixes a missconception about `/v1/livechat/tags/:tagId` returning 404 if tag is not found + +- ([#34975](https://github.com/RocketChat/Rocket.Chat/pull/34975)) Fixes issue where a invalid `Accounts_CustomFieldsToShowInUserInfo` value would break the ui + +- ([#33141](https://github.com/RocketChat/Rocket.Chat/pull/33141)) Fixes an issue where video conf message block wasn't considering display avatars preference + +- ([#35046](https://github.com/RocketChat/Rocket.Chat/pull/35046)) Fixes an issue which was preventing admins in enterprise workspaces from updating some premium layout settings. + +-
Updated dependencies [beec5663fd9a2fcf1f6777592fb2f38eef22eb24, eba8e364e4bef7ed71ebb527738515e8f7914ec7, d5175eeb5be81bab061e5ff8c6991c589bfeb0f4, 0df16c4ca50a6ad8613cfdc11a8ef6cb216fb6a4, 89964144e042c8d9282b51efd89e1e684077fdd7, 599fd932627e64ff84831f5972706f92db440438, 2921a6aa6f7c971a29c8209574cfb66432bc9f47, 1854c9bac22defa2f8cf5593062200171163aa19, f80ac66b006080313f4aa5a04706ff9c8790622b, 083fc49cf718e460dd6e8fcd72b98b42aeb6fc86, dee90e0791de41997e6df6149c4fe07d3a12c003, dd889ed2984c4c8a19a8b3cdb7ab7287a1258ea5, dac213d8c955d1e5dd1c8b434e07070dedecba2d, f85da08765a9d3f8c5aabd9291fd08be6dfdeb85, 271894fb3942d5d0ce3d669325d07fbbbc4bf112, 697a38d23590ac799f0f3c14a676fb6bea7e86ea, 30ea250f03331513029d812ab4c7841e712d1a73, be5031a21bdcda31270d53d319f7d183e77d84d7, 36e90a2eb2f9698f7ba42f6e8429a240114426bf]: + + - @rocket.chat/i18n@1.4.0-rc.0 + - @rocket.chat/rest-typings@7.4.0-rc.0 + - @rocket.chat/models@1.3.0-rc.0 + - @rocket.chat/model-typings@1.4.0-rc.0 + - @rocket.chat/network-broker@0.1.6-rc.0 + - @rocket.chat/fuselage-ui-kit@16.0.0-rc.0 + - @rocket.chat/ui-video-conf@16.0.0-rc.0 + - @rocket.chat/core-typings@7.4.0-rc.0 + - @rocket.chat/apps-engine@1.49.0-rc.0 + - @rocket.chat/ui-client@16.0.0-rc.0 + - @rocket.chat/apps@0.3.0-rc.0 + - @rocket.chat/ui-voip@6.0.0-rc.0 + - @rocket.chat/cas-validate@0.0.3-rc.0 + - @rocket.chat/omnichannel-services@0.3.11-rc.0 + - @rocket.chat/pdf-worker@0.2.11-rc.0 + - @rocket.chat/ui-contexts@16.0.0-rc.0 + - @rocket.chat/web-ui-registration@16.0.0-rc.0 + - @rocket.chat/presence@0.2.14-rc.0 + - @rocket.chat/api-client@0.2.14-rc.0 + - @rocket.chat/core-services@0.7.6-rc.0 + - @rocket.chat/cron@0.1.14-rc.0 + - @rocket.chat/instance-status@0.1.14-rc.0 + - @rocket.chat/license@1.0.5-rc.0 + - @rocket.chat/freeswitch@1.2.1-rc.0 + - @rocket.chat/gazzodown@16.0.0-rc.0 + - @rocket.chat/ui-theming@0.4.2 + - @rocket.chat/ui-avatar@12.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ +## 7.3.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#35212](https://github.com/RocketChat/Rocket.Chat/pull/35212) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes incorrect start date on omnichannel reports + +- ([#35222](https://github.com/RocketChat/Rocket.Chat/pull/35222) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes `channels.list` endpoint from rejecting pagination parameters + +- ([#35251](https://github.com/RocketChat/Rocket.Chat/pull/35251) by [@dionisio-bot](https://github.com/dionisio-bot)) fixes an issue with embedded layout rooms displaying as if the user is not part of the room + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.3.2 + - @rocket.chat/rest-typings@7.3.2 + - @rocket.chat/license@1.0.6 + - @rocket.chat/omnichannel-services@0.3.12 + - @rocket.chat/pdf-worker@0.2.12 + - @rocket.chat/presence@0.2.15 + - @rocket.chat/api-client@0.2.15 + - @rocket.chat/apps@0.2.6 + - @rocket.chat/core-services@0.7.7 + - @rocket.chat/cron@0.1.15 + - @rocket.chat/freeswitch@1.2.2 + - @rocket.chat/fuselage-ui-kit@15.0.2 + - @rocket.chat/gazzodown@15.0.2 + - @rocket.chat/model-typings@1.3.2 + - @rocket.chat/ui-contexts@15.0.2 + - @rocket.chat/models@1.2.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.7 + - @rocket.chat/ui-theming@0.4.2 + - @rocket.chat/ui-avatar@11.0.2 + - @rocket.chat/ui-client@15.0.2 + - @rocket.chat/ui-video-conf@15.0.2 + - @rocket.chat/ui-voip@5.0.2 + - @rocket.chat/web-ui-registration@15.0.2 + - @rocket.chat/instance-status@0.1.15 +
+ +## 7.3.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#35112](https://github.com/RocketChat/Rocket.Chat/pull/35112) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes the queue processing of Omnichannel's waiting queue focusing on 3 main areas: + - Changes the way we fetch the queue list to not append the public queue by default. This makes the server to not run the public queue always (as it is now) even if there was no work to be done. + - Changes how the queue executes: previously, it was executed in a kind of chain: We fetched a list of "queues", then we took one, processed it, and after that we scheduled the next run, which could take some time. Now, every TIMEOUT, server will try to process all the queues, 1 by 1, and then schedule the next run for all queues after TIMEOUT. This should speed up chat assignment and reduce waiting time when waiting queue is enabled. + - Removes the unlockAndRequeue and replcaes it with just unlock. This change shouldn't be noticeable. The original idea of the requeueing was to iterate over the inquiries when 1 wasn't being able to be taken. Idea was to avoid blocking the queue by rotating them instead of fetching the same until it gets routed, however this never worked cause we never modified the global sorting for the inquiries and it kept using the ts as the sorting, which returned always the oldest and ignored the requeing. So we're removing those extra steps as well. +- ([#35096](https://github.com/RocketChat/Rocket.Chat/pull/35096) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes a behavior in Omnichannel that was causing bot agents to be waiting in the queue, when they should always skip it. + +-
Updated dependencies [b7905dfebe48d27d0d774fb23cc579ea9dfd01f4]: + + - @rocket.chat/model-typings@1.3.1 + - @rocket.chat/models@1.2.1 + - @rocket.chat/omnichannel-services@0.3.11 + - @rocket.chat/apps@0.2.5 + - @rocket.chat/presence@0.2.14 + - @rocket.chat/core-services@0.7.6 + - @rocket.chat/cron@0.1.14 + - @rocket.chat/instance-status@0.1.14 + - @rocket.chat/network-broker@0.1.6 + - @rocket.chat/core-typings@7.3.1 + - @rocket.chat/rest-typings@7.3.1 + - @rocket.chat/license@1.0.5 + - @rocket.chat/pdf-worker@0.2.11 + - @rocket.chat/api-client@0.2.14 + - @rocket.chat/freeswitch@1.2.1 + - @rocket.chat/fuselage-ui-kit@15.0.1 + - @rocket.chat/gazzodown@15.0.1 + - @rocket.chat/ui-contexts@15.0.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/ui-theming@0.4.2 + - @rocket.chat/ui-avatar@11.0.1 + - @rocket.chat/ui-client@15.0.1 + - @rocket.chat/ui-video-conf@15.0.1 + - @rocket.chat/ui-voip@5.0.1 + - @rocket.chat/web-ui-registration@15.0.1 +
+ ## 7.3.0 ### Minor Changes diff --git a/apps/meteor/app/2fa/server/methods/disable.ts b/apps/meteor/app/2fa/server/methods/disable.ts index d2c71febdc357..9e64afb48f20e 100644 --- a/apps/meteor/app/2fa/server/methods/disable.ts +++ b/apps/meteor/app/2fa/server/methods/disable.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { TOTP } from '../lib/totp'; declare module '@rocket.chat/ddp-client' { @@ -26,7 +27,7 @@ Meteor.methods({ }); } - if (!user.services?.totp) { + if (!user.services?.totp?.enabled) { return false; } @@ -41,6 +42,14 @@ Meteor.methods({ return false; } - return (await Users.disable2FAByUserId(userId)).modifiedCount > 0; + const { modifiedCount } = await Users.disable2FAByUserId(userId); + + if (!modifiedCount) { + return false; + } + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { 'services.totp.enabled': false } }); + + return true; }, }); diff --git a/apps/meteor/app/2fa/server/methods/validateTempToken.ts b/apps/meteor/app/2fa/server/methods/validateTempToken.ts index 840fadc8cbf7c..89338acd9730f 100644 --- a/apps/meteor/app/2fa/server/methods/validateTempToken.ts +++ b/apps/meteor/app/2fa/server/methods/validateTempToken.ts @@ -2,7 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; +import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { TOTP } from '../lib/totp'; declare module '@rocket.chat/ddp-client' { @@ -56,13 +56,18 @@ Meteor.methods({ if (!this.userId) { return; } - const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + const user = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1, 'services.totp': 1 } }); return { clientAction: 'updated', id: this.userId, - diff: { 'services.resume.loginTokens': userTokens?.services?.resume?.loginTokens }, + diff: { + 'services.resume.loginTokens': user?.services?.resume?.loginTokens, + ...(user?.services?.totp && { 'services.totp.enabled': user.services.totp.enabled }), + }, }; }); + } else { + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { 'services.totp.enabled': true } }); } } diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 7854138c063be..cd274fe8f72cb 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -705,7 +705,7 @@ export class APIClass { this._routes.push({ path: route, options: _options, - endpoints: operations[method as keyof Operations] as Record, + endpoints: operations[method as keyof Operations] as unknown as Record, }); }); }); @@ -1014,7 +1014,7 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => }); Meteor.startup(() => { - (WebApp.connectHandlers as ReturnType).use( + (WebApp.connectHandlers as unknown as ReturnType).use( API.api .use((_req, res, next) => { res.removeHeader('X-Powered-By'); @@ -1029,7 +1029,7 @@ Meteor.startup(() => { ); }); -(WebApp.connectHandlers as ReturnType) +(WebApp.connectHandlers as unknown as ReturnType) .use( express.json({ limit: '50mb', diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 6176d2644b471..152e454b99ded 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -94,7 +94,7 @@ export class Router< { urlParams: req.params, queryParams: req.query, - bodyParams: req.body, + bodyParams: (req as any).bodyParams || req.body, request: req, response: res, } as any, diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 4f473477f25a8..d46ecda1a0ac8 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,5 @@ import { Team, Room } from '@rocket.chat/core-services'; -import type { IRoom, ISubscription, IUser, RoomType } from '@rocket.chat/core-typings'; +import { TEAM_TYPE, type IRoom, type ISubscription, type IUser, type RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isChannelsAddAllProps, @@ -36,6 +36,7 @@ import { saveRoomSettings } from '../../../channel-settings/server/methods/saveR import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { addUsersToRoomMethod } from '../../../lib/server/methods/addUsersToRoom'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; +import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -163,10 +164,11 @@ API.v1.addRoute( const { count = 20, offset = 0 } = await getPaginationItems(this.queryParams); - const result = await Meteor.callAsync('getChannelHistory', { + const result = await getChannelHistory({ rid: findResult._id, + fromUserId: this.userId, latest: latest ? new Date(latest) : new Date(), - oldest: oldest && new Date(oldest), + oldest: oldest ? new Date(oldest) : undefined, inclusive: inclusive === 'true', offset, count, @@ -300,6 +302,10 @@ API.v1.addRoute( ...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}), }; + if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.forbidden(); + } + // Special check for the permissions if ( (await hasPermissionAsync(this.userId, 'view-joined-room')) && @@ -451,6 +457,10 @@ API.v1.addRoute( const findResult = await findChannelByIdOrName({ params }); + if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.forbidden(); + } + const moderators = ( await Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], { projection: { u: 1 }, @@ -857,6 +867,10 @@ API.v1.addRoute( checkedArchived: false, }); + if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.forbidden(); + } + let includeAllPublicChannels = true; if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') { includeAllPublicChannels = this.queryParams.includeAllPublicChannels === 'true'; @@ -902,12 +916,18 @@ API.v1.addRoute( { authRequired: true }, { async get() { + const findResult = await findChannelByIdOrName({ + params: this.queryParams, + checkedArchived: false, + userId: this.userId, + }); + + if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.forbidden(); + } + return API.v1.success({ - channel: await findChannelByIdOrName({ - params: this.queryParams, - checkedArchived: false, - userId: this.userId, - }), + channel: findResult, }); }, }, @@ -1056,6 +1076,10 @@ API.v1.addRoute( checkedArchived: false, }); + if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.forbidden(); + } + if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) { return API.v1.forbidden(); } @@ -1414,7 +1438,7 @@ API.v1.addRoute( API.v1.addRoute( 'channels.anonymousread', - { authRequired: false }, + { authOrAnonRequired: true }, { async get() { const findResult = await findChannelByIdOrName({ @@ -1432,6 +1456,16 @@ API.v1.addRoute( }); } + // Public rooms of private teams should be accessible only by team members + if (findResult.teamId) { + const team = await Team.getOneById(findResult.teamId); + if (team?.type === TEAM_TYPE.PRIVATE) { + if (!this.userId || !(await canAccessRoomAsync(findResult, { _id: this.userId }))) { + return API.v1.notFound('Room not found'); + } + } + } + const { cursor, totalCount } = await Messages.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index dd85bdce7ee18..7569f321fa203 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -8,6 +8,27 @@ import { isChatGetThreadsListProps, isChatDeleteProps, isChatSyncMessagesProps, + isChatGetMessageProps, + isChatPinMessageProps, + isChatPostMessageProps, + isChatSearchProps, + isChatSendMessageProps, + isChatStarMessageProps, + isChatUnpinMessageProps, + isChatUnstarMessageProps, + isChatIgnoreUserProps, + isChatGetPinnedMessagesProps, + isChatFollowMessageProps, + isChatUnfollowMessageProps, + isChatGetMentionedMessagesProps, + isChatOTRProps, + isChatReactProps, + isChatGetDeletedMessagesProps, + isChatSyncThreadsListProps, + isChatGetThreadMessagesProps, + isChatSyncThreadMessagesProps, + isChatGetStarredMessagesProps, + isChatGetDiscussionsProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -15,6 +36,7 @@ import { Meteor } from 'meteor/meteor'; import { reportMessage } from '../../../../server/lib/moderation/reportMessage'; import { ignoreUser } from '../../../../server/methods/ignoreUser'; import { messageSearch } from '../../../../server/methods/messageSearch'; +import { getMessageHistory } from '../../../../server/publications/messages'; import { roomAccessAttributes } from '../../../authorization/server'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; @@ -106,7 +128,7 @@ API.v1.addRoute( ...(type && { type }), }; - const result = await Meteor.callAsync('messages/get', roomId, getMessagesQuery); + const result = await getMessageHistory(roomId, this.userId, getMessagesQuery); if (!result) { return API.v1.failure(); @@ -114,9 +136,9 @@ API.v1.addRoute( return API.v1.success({ result: { - ...(result.updated && { updated: await normalizeMessagesForUser(result.updated, this.userId) }), - ...(result.deleted && { deleted: result.deleted }), - ...(result.cursor && { cursor: result.cursor }), + updated: 'updated' in result ? await normalizeMessagesForUser(result.updated, this.userId) : [], + deleted: 'deleted' in result ? result.deleted : [], + cursor: 'cursor' in result ? result.cursor : undefined, }, }); }, @@ -127,6 +149,7 @@ API.v1.addRoute( 'chat.getMessage', { authRequired: true, + validateParams: isChatGetMessageProps, }, { async get() { @@ -151,13 +174,9 @@ API.v1.addRoute( API.v1.addRoute( 'chat.pinMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatPinMessageProps }, { async post() { - if (!this.bodyParams.messageId?.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); - } - const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { @@ -177,7 +196,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.postMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatPostMessageProps }, { async post() { const { text, attachments } = this.bodyParams; @@ -214,7 +233,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.search', - { authRequired: true }, + { authRequired: true, validateParams: isChatSearchProps }, { async get() { const { roomId, searchText } = this.queryParams; @@ -249,13 +268,9 @@ API.v1.addRoute( // one channel whereas the other one allows for sending to more than one channel at a time. API.v1.addRoute( 'chat.sendMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatSendMessageProps }, { async post() { - if (!this.bodyParams.message) { - throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.'); - } - if (MessageTypes.isSystemMessage(this.bodyParams.message)) { throw new Error("Cannot send system messages using 'chat.sendMessage'"); } @@ -274,13 +289,9 @@ API.v1.addRoute( API.v1.addRoute( 'chat.starMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatStarMessageProps }, { async post() { - if (!this.bodyParams.messageId?.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); - } - const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { @@ -300,13 +311,9 @@ API.v1.addRoute( API.v1.addRoute( 'chat.unPinMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatUnpinMessageProps }, { async post() { - if (!this.bodyParams.messageId?.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); - } - const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { @@ -322,13 +329,9 @@ API.v1.addRoute( API.v1.addRoute( 'chat.unStarMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatUnstarMessageProps }, { async post() { - if (!this.bodyParams.messageId?.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); - } - const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { @@ -390,13 +393,9 @@ API.v1.addRoute( API.v1.addRoute( 'chat.react', - { authRequired: true }, + { authRequired: true, validateParams: isChatReactProps }, { async post() { - if (!this.bodyParams.messageId?.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); - } - const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { @@ -439,7 +438,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.ignoreUser', - { authRequired: true }, + { authRequired: true, validateParams: isChatIgnoreUserProps }, { async get() { const { rid, userId } = this.queryParams; @@ -464,23 +463,13 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getDeletedMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetDeletedMessagesProps }, { async get() { const { roomId, since } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); - if (!roomId) { - throw new Meteor.Error('The required "roomId" query param is missing.'); - } - - if (!since) { - throw new Meteor.Error('The required "since" query param is missing.'); - } else if (isNaN(Date.parse(since))) { - throw new Meteor.Error('The "since" query parameter must be a valid date.'); - } - - const { cursor, totalCount } = await Messages.trashFindPaginatedDeletedAfter( + const { cursor, totalCount } = Messages.trashFindPaginatedDeletedAfter( new Date(since), { rid: roomId }, { @@ -504,21 +493,17 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getPinnedMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetPinnedMessagesProps }, { async get() { const { roomId } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); - if (!roomId) { - throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); - } - if (!(await canAccessRoomIdAsync(roomId, this.userId))) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const { cursor, totalCount } = await Messages.findPaginatedPinnedByRoom(roomId, { + const { cursor, totalCount } = Messages.findPaginatedPinnedByRoom(roomId, { skip: offset, limit: count, }); @@ -584,7 +569,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.syncThreadsList', - { authRequired: true }, + { authRequired: true, validateParams: isChatSyncThreadsListProps }, { async get() { const { rid } = this.queryParams; @@ -594,12 +579,7 @@ API.v1.addRoute( if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } - if (!rid) { - throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" query param is missing.'); - } - if (!updatedSince) { - throw new Meteor.Error('error-updatedSince-param-invalid', 'The required param "updatedSince" is missing.'); - } + if (isNaN(Date.parse(updatedSince))) { throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); } else { @@ -633,7 +613,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getThreadMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetThreadMessagesProps }, { async get() { const { tmid } = this.queryParams; @@ -643,9 +623,7 @@ API.v1.addRoute( if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } - if (!tmid) { - throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.'); - } + const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } }); if (!thread?.rid) { throw new Meteor.Error('error-invalid-message', 'Invalid Message'); @@ -656,7 +634,7 @@ API.v1.addRoute( if (!room || !user || !(await canAccessRoomAsync(room, user))) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } - const { cursor, totalCount } = await Messages.findPaginated( + const { cursor, totalCount } = Messages.findPaginated( { ...query, tmid }, { sort: sort || { ts: 1 }, @@ -680,7 +658,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.syncThreadMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatSyncThreadMessagesProps }, { async get() { const { tmid } = this.queryParams; @@ -690,12 +668,7 @@ API.v1.addRoute( if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } - if (!tmid) { - throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.'); - } - if (!updatedSince) { - throw new Meteor.Error('error-updatedSince-param-invalid', 'The required param "updatedSince" is missing.'); - } + if (isNaN(Date.parse(updatedSince))) { throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); } else { @@ -705,6 +678,7 @@ API.v1.addRoute( if (!thread?.rid) { throw new Meteor.Error('error-invalid-message', 'Invalid Message'); } + // TODO: promise.all? this.user? const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); @@ -723,7 +697,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.followMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatFollowMessageProps }, { async post() { const { mid } = this.bodyParams; @@ -741,7 +715,7 @@ API.v1.addRoute( API.v1.addRoute( 'chat.unfollowMessage', - { authRequired: true }, + { authRequired: true, validateParams: isChatUnfollowMessageProps }, { async post() { const { mid } = this.bodyParams; @@ -759,15 +733,13 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getMentionedMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetMentionedMessagesProps }, { async get() { const { roomId } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); - if (!roomId) { - throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); - } + const messages = await findMentionedMessages({ uid: this.userId, roomId, @@ -785,16 +757,13 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getStarredMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetStarredMessagesProps }, { async get() { const { roomId } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); - if (!roomId) { - throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); - } const messages = await findStarredMessages({ uid: this.userId, roomId, @@ -814,16 +783,13 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getDiscussions', - { authRequired: true }, + { authRequired: true, validateParams: isChatGetDiscussionsProps }, { async get() { const { roomId, text } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); - if (!roomId) { - throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); - } const messages = await findDiscussionsFromRoom({ uid: this.userId, roomId, @@ -841,19 +807,11 @@ API.v1.addRoute( API.v1.addRoute( 'chat.otr', - { authRequired: true }, + { authRequired: true, validateParams: isChatOTRProps }, { async post() { const { roomId, type: otrType } = this.bodyParams; - if (!roomId) { - throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); - } - - if (!otrType) { - throw new Meteor.Error('error-invalid-params', 'The required "type" query param is missing.'); - } - const { username, type } = this.user; if (!username) { diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index f33c9a6db55aa..df78441955480 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -16,6 +16,7 @@ import { hasAllPermissionAsync, hasPermissionAsync } from '../../../authorizatio import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; +import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; @@ -505,8 +506,9 @@ API.v1.addRoute( const showThreadMessages = this.queryParams.showThreadMessages !== 'false'; - const result = await Meteor.callAsync('getChannelHistory', { + const result = await getChannelHistory({ rid: findResult.rid, + fromUserId: this.userId, latest: latestDate, oldest: oldestDate, inclusive, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 765e3c896f75a..dc762a6f2b78d 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -21,6 +21,7 @@ import { canAccessRoomIdAsync } from '../../../authorization/server/functions/ca import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; +import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; @@ -277,8 +278,9 @@ API.v1.addRoute( const objectParams = { rid: room._id, + fromUserId: this.userId, latest: latest ? new Date(latest) : new Date(), - oldest: oldest && new Date(oldest), + oldest: oldest ? new Date(oldest) : undefined, inclusive: inclusive === 'true', offset, count, @@ -286,7 +288,7 @@ API.v1.addRoute( showThreadMessages: showThreadMessages === 'true', }; - const result = await Meteor.callAsync('getChannelHistory', objectParams); + const result = await getChannelHistory(objectParams); if (!result) { return API.v1.forbidden(); @@ -400,7 +402,7 @@ API.v1.addRoute( ...parseIds(starredIds, 'starred._id'), ...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}), }; - const sortObj = { ts: sort?.ts ?? -1 }; + const sortObj = sort || { ts: -1 }; const { cursor, totalCount } = Messages.findPaginated(ourQuery, { sort: sortObj, diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 589b7af3d8838..e4d10886dff76 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -12,6 +12,7 @@ import { isRoomsCleanHistoryProps, isRoomsOpenProps, isRoomsMembersOrderedByRoleProps, + isRoomsHideProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -21,6 +22,7 @@ import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole'; import { openRoom } from '../../../../server/lib/openRoom'; +import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; @@ -962,3 +964,31 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'rooms.hide', + { authRequired: true, validateParams: isRoomsHideProps }, + { + async post() { + const { roomId } = this.bodyParams; + + if (!(await canAccessRoomIdAsync(roomId, this.userId))) { + return API.v1.unauthorized(); + } + + const user = await Users.findOneById(this.userId, { projections: { _id: 1 } }); + + if (!user) { + return API.v1.failure('error-invalid-user'); + } + + const modCount = await hideRoomMethod(this.userId, roomId); + + if (!modCount) { + return API.v1.failure('error-room-already-hidden'); + } + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index be0aaea4b4e1d..200fa7a5eabb9 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -8,6 +8,8 @@ import { import { Meteor } from 'meteor/meteor'; import { readMessages } from '../../../../server/lib/readMessages'; +import { getSubscriptions } from '../../../../server/publications/subscription'; +import { unreadMessages } from '../../../message-mark-as-unread/server/unreadMessages'; import { API } from '../api'; API.v1.addRoute( @@ -28,7 +30,7 @@ API.v1.addRoute( updatedSinceDate = new Date(updatedSince as string); } - const result = await Meteor.callAsync('subscriptions/get', updatedSinceDate); + const result = await getSubscriptions(this.userId, updatedSinceDate); return API.v1.success( Array.isArray(result) @@ -98,7 +100,7 @@ API.v1.addRoute( }, { async post() { - await Meteor.callAsync('unreadMessages', (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); + await unreadMessages(this.userId, (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 071517822e33c..7858a236f8685 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -25,6 +25,9 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; +import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken'; +import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken'; +import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail'; @@ -793,11 +796,11 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true }, { async post() { - const { tokenName, bypassTwoFactor } = this.bodyParams; + const { tokenName, bypassTwoFactor = false } = this.bodyParams; if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - const token = await Meteor.callAsync('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }); + const token = await generatePersonalAccessTokenOfUser({ tokenName, userId: this.userId, bypassTwoFactor }); return API.v1.success({ token }); }, @@ -813,7 +816,7 @@ API.v1.addRoute( if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - const token = await Meteor.callAsync('personalAccessTokens:regenerateToken', { tokenName }); + const token = await regeneratePersonalAccessTokenOfUser(tokenName, this.userId); return API.v1.success({ token }); }, @@ -852,9 +855,7 @@ API.v1.addRoute( if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - await Meteor.callAsync('personalAccessTokens:removeToken', { - tokenName, - }); + await removePersonalAccessTokenOfUser(tokenName, this.userId); return API.v1.success(); }, @@ -888,15 +889,15 @@ API.v1.addRoute( // TODO this can be optmized so places that care about loginTokens being removed are invoked directly // instead of having to listen to every watch.users event void notifyOnUserChangeAsync(async () => { - const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); - if (!userTokens) { + const user = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1, 'services.email2fa': 1 } }); + if (!user) { return; } return { clientAction: 'updated', id: this.user._id, - diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, + diff: { 'services.resume.loginTokens': user.services?.resume?.loginTokens, 'services.email2fa': user.services?.email2fa }, }; }); @@ -912,6 +913,19 @@ API.v1.addRoute( async post() { await Users.disableEmail2FAByUserId(this.userId); + void notifyOnUserChangeAsync(async () => { + const user = await Users.findOneById(this.userId, { projection: { 'services.email2fa': 1 } }); + if (!user) { + return; + } + + return { + clientAction: 'updated', + id: this.user._id, + diff: { 'services.email2fa': user.services?.email2fa }, + }; + }); + return API.v1.success(); }, }, diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 13db1179310c5..d18d5bdceb656 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -10,6 +10,7 @@ export class AppListenerBridge { // eslint-disable-next-line complexity const method = (() => { switch (event) { + case AppInterface.IPostSystemMessageSent: case AppInterface.IPreMessageSentPrevent: case AppInterface.IPreMessageSentExtend: case AppInterface.IPreMessageSentModify: diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index f2521d2f8cf5d..f1555bd38805f 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -11,6 +11,7 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { settings } from '../../../settings/server'; @@ -145,7 +146,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor && { visitor }), }; - await LivechatTyped.closeRoom(closeData); + await closeRoom(closeData); return true; } diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 824a9d5c15afd..16366626c07c8 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -5,7 +5,7 @@ import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/ import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; -import { Users, Subscriptions, Messages } from '@rocket.chat/models'; +import { Users, Subscriptions } from '@rocket.chat/models'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; @@ -44,12 +44,8 @@ export class AppMessageBridge extends MessageBridge { throw new Error('Invalid editor assigned to the message for the update.'); } - if (!message.id || !(await Messages.findOneById(message.id))) { - throw new Error('A message must exist to update.'); - } - // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. - const msg: IMessage | undefined = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message, true); const editor = await Users.findOneById(message.editor.id); if (!editor) { diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index d88881330a17d..ea8a7fdf6e7e6 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -163,13 +163,13 @@ export class AppRoomBridge extends RoomBridge { protected async update(room: IRoom, members: Array = [], appId: string): Promise { this.orch.debugLog(`The App ${appId} is updating a room.`); - if (!room.id || !(await Rooms.findOneById(room.id))) { - throw new Error('A room must exist to update.'); - } + const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room, true); - const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room); + const updateResult = await Rooms.updateOne({ _id: room.id }, { $set: rm }); - await Rooms.updateOne({ _id: rm._id }, { $set: rm as Partial }); + if (!updateResult.matchedCount) { + throw new Error('Room id not found'); + } for await (const username of members) { const member = await Users.findOneByUsername(username, {}); @@ -178,7 +178,7 @@ export class AppRoomBridge extends RoomBridge { continue; } - await addUserToRoom(rm._id, member); + await addUserToRoom(room.id, member); } } diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index b3e6aaff32a47..5a5b1d902d4a9 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -1,4 +1,5 @@ -import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { Apps, type IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { IReadSettingPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; @@ -21,11 +22,12 @@ export class AppSettingBridge extends ServerSettingBridge { protected async getOneById(id: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the setting by id ${id}.`); - if (!(await this.isReadableById(id, appId))) { + const setting = await this.getReadableSettingById(id, appId); + if (!setting) { throw new Error(`The setting "${id}" is not readable.`); } - return this.orch.getConverters()?.get('settings').convertById(id); + return setting; } protected async hideGroup(name: string, appId: string): Promise { @@ -50,6 +52,39 @@ export class AppSettingBridge extends ServerSettingBridge { return Boolean(setting && !setting.secret); } + protected async getReadableSettingById(id: string, appId: string): Promise { + this.orch.debugLog(`The app ${appId} is checking if it can read the setting ${id}`); + const app = Apps.self?.getManager().getOneById(appId); + if (!app) { + this.orch.debugLog(`The app ${appId} is not found.`); + return null; + } + + const { permissions } = app.getInfo(); + if (!permissions) { + this.orch.debugLog(`The app ${appId} has no configured permissions.`); + return null; + } + + const readSettingsPermission = permissions.find((perm) => perm.name === 'server-setting.read'); + if (!readSettingsPermission) { + this.orch.debugLog(`The app ${appId} has no server-setting.read permission.`); + return null; + } + + const readSettings = readSettingsPermission as IReadSettingPermission; + // If the setting is in the hiddenSettings list (defined within the permission), then it can bypass the hidden flag. + // If not, then it must be a non-hidden setting. This is to allow apps to read hidden settings if they have the permission to do so. + const setting = readSettings.hiddenSettings?.includes(id) ? await Settings.findOneById(id) : await Settings.findOneNotHiddenById(id); + + if (!setting) { + this.orch.debugLog(`The setting ${id} is not found.`); + return null; + } + + return this.orch.getConverters()?.get('settings').convertToApp(setting); + } + protected async updateOne(setting: ISetting & { id: string }, appId: string): Promise { this.orch.debugLog(`The App ${appId} is updating the setting ${setting.id} .`); diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 704dde1850490..8cc6f4ea270f2 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -48,6 +48,7 @@ export class AppMessagesConverter { attachments: getAttachments, sender: 'u', threadMsgCount: 'tcount', + type: 't', }; return transformMappedData(message, map); @@ -91,6 +92,7 @@ export class AppMessagesConverter { groupable: 'groupable', token: 'token', blocks: 'blocks', + type: 't', room: async (message) => { const result = await cache.get('room')(message.rid); delete message.rid; @@ -135,19 +137,23 @@ export class AppMessagesConverter { return transformMappedData(msgObj, map); } - async convertAppMessage(message) { - if (!message || !message.room) { + async convertAppMessage(message, isPartial = false) { + if (!message) { return undefined; } - const room = await Rooms.findOneById(message.room.id); + let rid; + if (message.room?.id) { + const room = await Rooms.findOneById(message.room.id, { projection: { _id: 1 } }); + rid = room?._id; + } - if (!room) { + if (!rid && !isPartial) { throw new Error('Invalid room provided on the message.'); } let u; - if (message.sender && message.sender.id) { + if (message.sender?.id) { const user = await Users.findOneById(message.sender.id); if (user) { @@ -176,14 +182,27 @@ export class AppMessagesConverter { const attachments = this._convertAppAttachments(message.attachments); + let _id = message.id; + let ts = message.createdAt; + + if (!isPartial) { + if (!message.id) { + _id = Random.id(); + } + + if (!message.createdAt) { + ts = new Date(); + } + } + const newMessage = { - _id: message.id || Random.id(), + _id, ...('threadId' in message && { tmid: message.threadId }), - rid: room._id, + rid, u, msg: message.text, - ts: message.createdAt || new Date(), - _updatedAt: message.updatedAt || new Date(), + ts, + _updatedAt: message.updatedAt, ...(editedBy && { editedBy }), ...('editedAt' in message && { editedAt: message.editedAt }), ...('emoji' in message && { emoji: message.emoji }), @@ -198,7 +217,17 @@ export class AppMessagesConverter { ...('token' in message && { token: message.token }), }; - return Object.assign(newMessage, message._unmappedProperties_); + if (isPartial) { + Object.entries(newMessage).forEach(([key, value]) => { + if (typeof value === 'undefined') { + delete newMessage[key]; + } + }); + } else { + Object.assign(newMessage, message._unmappedProperties_); + } + + return newMessage; } _convertAppAttachments(attachments) { diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 741f989321916..b2bbcda49610d 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,7 +20,7 @@ export class AppRoomsConverter { return this.convertRoom(room); } - async convertAppRoom(room) { + async convertAppRoom(room, isPartial = false) { if (!room) { return undefined; } @@ -68,11 +68,18 @@ export class AppRoomsConverter { let closedBy; if (room.closedBy) { - const user = await Users.findOneById(room.closedBy.id); - closedBy = { - _id: user._id, - username: user.username, - }; + if (room.closer === 'user') { + const user = await Users.findOneById(room.closedBy.id); + closedBy = { + _id: user._id, + username: user.username, + }; + } else if (room.closer === 'visitor') { + closedBy = { + _id: v._id, + username: v.username, + }; + } } let contactId; @@ -81,6 +88,26 @@ export class AppRoomsConverter { contactId = contact._id; } + let _default; + if (typeof room.isDefault !== 'undefined') { + _default = room.isDefault; + } + + let ro; + if (typeof room.isReadOnly !== 'undefined') { + ro = room.isReadOnly; + } + + let sysMes; + if (typeof room.displaySystemMessages !== 'undefined') { + sysMes = room.displaySystemMessages; + } + + let msgs; + if (typeof room.messageCount !== 'undefined') { + msgs = room.messageCount; + } + const newRoom = { ...(room.id && { _id: room.id }), fname: room.displayName, @@ -88,17 +115,17 @@ export class AppRoomsConverter { t: room.type, u, v, + ro, + sysMes, + msgs, departmentId, servedBy, closedBy, members: room.members, uids: room.userIds, - default: typeof room.isDefault === 'undefined' ? false : room.isDefault, - ro: typeof room.isReadOnly === 'undefined' ? false : room.isReadOnly, - sysMes: typeof room.displaySystemMessages === 'undefined' ? true : room.displaySystemMessages, + default: _default, waitingResponse: typeof room.isWaitingResponse === 'undefined' ? undefined : !!room.isWaitingResponse, open: typeof room.isOpen === 'undefined' ? undefined : !!room.isOpen, - msgs: room.messageCount || 0, ts: room.createdAt, _updatedAt: room.updatedAt, closedAt: room.closedAt, @@ -115,7 +142,17 @@ export class AppRoomsConverter { }), }; - return Object.assign(newRoom, room._unmappedProperties_); + if (isPartial) { + Object.entries(newRoom).forEach(([key, value]) => { + if (typeof value === 'undefined') { + delete newRoom[key]; + } + }); + } else { + Object.assign(newRoom, room._unmappedProperties_); + } + + return newRoom; } async convertRoom(originalRoom) { @@ -238,6 +275,7 @@ export class AppRoomsConverter { if (originalRoom.closer === 'user') { return this.orch.getConverters().get('users').convertById(closedBy._id); } + return this.orch.getConverters().get('visitors').convertById(closedBy._id); }, servedBy: async (room) => { diff --git a/apps/meteor/app/apps/server/converters/settings.js b/apps/meteor/app/apps/server/converters/settings.js index da3e075deb678..07b790cb7c592 100644 --- a/apps/meteor/app/apps/server/converters/settings.js +++ b/apps/meteor/app/apps/server/converters/settings.js @@ -7,7 +7,7 @@ export class AppSettingsConverter { } async convertById(settingId) { - const setting = await Settings.findOneNotHiddenById(settingId); + const setting = await Settings.findOneById(settingId); return this.convertToApp(setting); } diff --git a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts index 77f8c54e2f494..3a219ad30cc62 100644 --- a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts +++ b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts @@ -65,7 +65,7 @@ Meteor.methods({ // prevent removing last user from admin role if (role._id === 'admin') { - const adminCount = await Users.col.countDocuments({ + const adminCount = await Users.countDocuments({ roles: { $in: ['admin'], }, diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index ad0dffca27d87..b14d3a77ec4a0 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -59,13 +59,13 @@ export const handleResponse = async (promise: Promise) => { const cacheValueInSettings = ( key: string, - fn: () => Promise, + fn: (retry?: number) => Promise, ): (() => Promise) & { - reset: () => Promise; + reset: (retry?: number) => Promise; } => { - const reset = async () => { + const reset = async (retry?: number) => { SystemLogger.debug(`Resetting cached value ${key} in settings`); - const value = await fn(); + const value = await fn(retry); if ( ( @@ -129,27 +129,26 @@ const getSupportedVersionsFromCloud = async () => { return response; }; -const getSupportedVersionsToken = async () => { +const getSupportedVersionsToken = async (retry = 0) => { /** * Gets the supported versions from the license * Gets the supported versions from the cloud * Gets the latest version * return the token */ - - const [versionsFromLicense, response] = await Promise.all([License.getLicense(), getSupportedVersionsFromCloud()]); + const [versionsFromLicense, cloudResponse] = await Promise.all([License.getLicense(), getSupportedVersionsFromCloud()]); const supportedVersions = await supportedVersionsChooseLatest( supportedVersionsFromBuild, versionsFromLicense?.supportedVersions, - (response.success && response.result) || undefined, + (cloudResponse.success && cloudResponse.result) || undefined, ); SystemLogger.debug({ msg: 'Supported versions', supportedVersionsFromBuild: supportedVersionsFromBuild.timestamp, versionsFromLicense: versionsFromLicense?.supportedVersions?.timestamp, - response: response.success && response.result?.timestamp, + response: cloudResponse.success && cloudResponse.result?.timestamp, }); switch (supportedVersions) { @@ -163,14 +162,28 @@ const getSupportedVersionsToken = async () => { msg: 'Using supported versions from license', }); break; - case response.success && response.result: + case cloudResponse.success && cloudResponse.result: SystemLogger.info({ msg: 'Using supported versions from cloud', }); break; } - await buildVersionUpdateMessage(supportedVersions?.versions); + // to avoid a possibly wrong message, we only send the message if the cloud response was successful + if (cloudResponse.success) { + await buildVersionUpdateMessage(supportedVersions?.versions); + } else if (retry < 5) { + // in case of failure we'll try again later + setTimeout( + async () => { + await getCachedSupportedVersionsToken.reset(retry + 1); + }, + 5000 * Math.pow(2, retry), + ); + } else { + SystemLogger.error(`Failed to get supported versions from cloud after ${retry} retries.`); + await buildVersionUpdateMessage(supportedVersions?.versions); + } return supportedVersions?.signed; }; diff --git a/apps/meteor/app/dolphin/client/hooks/useDolphin.ts b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts new file mode 100644 index 0000000000000..7a9d700238c95 --- /dev/null +++ b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts @@ -0,0 +1,31 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth'; + +const config = { + serverURL: '', + authorizePath: '/m/oauth2/auth/', + tokenPath: '/m/oauth2/token/', + identityPath: '/m/oauth2/api/me/', + scope: 'basic', + addAutopublishFields: { + forLoggedInUser: ['services.dolphin'], + forOtherUsers: ['services.dolphin.name'], + }, + accessTokenParam: 'access_token', +}; + +const Dolphin = new CustomOAuth('dolphin', config); + +export const useDolphin = () => { + const enabled = useSetting('Accounts_OAuth_Dolphin'); + const url = useSetting('Accounts_OAuth_Dolphin_URL') as string; + + useEffect(() => { + if (enabled) { + config.serverURL = url; + Dolphin.configure(config); + } + }, [enabled, url]); +}; diff --git a/apps/meteor/app/dolphin/client/index.ts b/apps/meteor/app/dolphin/client/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/dolphin/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/dolphin/client/lib.ts b/apps/meteor/app/dolphin/client/lib.ts deleted file mode 100644 index 31a767dd55561..0000000000000 --- a/apps/meteor/app/dolphin/client/lib.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; -import { settings } from '../../settings/client'; - -const config = { - serverURL: '', - authorizePath: '/m/oauth2/auth/', - tokenPath: '/m/oauth2/token/', - identityPath: '/m/oauth2/api/me/', - scope: 'basic', - addAutopublishFields: { - forLoggedInUser: ['services.dolphin'], - forOtherUsers: ['services.dolphin.name'], - }, - accessTokenParam: 'access_token', -}; - -const Dolphin = new CustomOAuth('dolphin', config); - -Meteor.startup(() => - Tracker.autorun(() => { - if (settings.get('Accounts_OAuth_Dolphin_URL')) { - config.serverURL = settings.get('Accounts_OAuth_Dolphin_URL'); - return Dolphin.configure(config); - } - }), -); diff --git a/apps/meteor/app/drupal/client/lib.ts b/apps/meteor/app/drupal/client/hooks/useDrupal.ts similarity index 68% rename from apps/meteor/app/drupal/client/lib.ts rename to apps/meteor/app/drupal/client/hooks/useDrupal.ts index f477a326d7062..33295b33684a3 100644 --- a/apps/meteor/app/drupal/client/lib.ts +++ b/apps/meteor/app/drupal/client/hooks/useDrupal.ts @@ -1,9 +1,8 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; -import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; -import { settings } from '../../settings/client'; +import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth'; // Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal // In RocketChat -> Administration the URL needs to be http(s)://{drupal.server}/ @@ -26,11 +25,13 @@ const config: OauthConfig = { const Drupal = new CustomOAuth('drupal', config); -Meteor.startup(() => { - Tracker.autorun(() => { - if (settings.get('API_Drupal_URL')) { - config.serverURL = settings.get('API_Drupal_URL'); +export const useDrupal = () => { + const drupalUrl = useSetting('API_Drupal_URL') as string; + + useEffect(() => { + if (drupalUrl) { + config.serverURL = drupalUrl; Drupal.configure(config); } - }); -}); + }, [drupalUrl]); +}; diff --git a/apps/meteor/app/drupal/client/index.ts b/apps/meteor/app/drupal/client/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/drupal/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts new file mode 100644 index 0000000000000..6dfd82c17cfb5 --- /dev/null +++ b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts @@ -0,0 +1,51 @@ +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { emoji } from '../../../emoji/client'; +import { getEmojiConfig } from '../../lib/getEmojiConfig'; +import { isSetNotNull } from '../../lib/isSetNotNull'; + +const config = getEmojiConfig(); + +export const useEmojiOne = () => { + const convertAsciiToEmoji = useUserPreference('convertAsciiEmoji', true); + + emoji.packages.emojione = config.emojione as any; + if (emoji.packages.emojione) { + emoji.packages.emojione.sprites = config.sprites; + emoji.packages.emojione.emojisByCategory = config.emojisByCategory; + emoji.packages.emojione.emojiCategories = config.emojiCategories as any; + emoji.packages.emojione.toneList = config.toneList; + + emoji.packages.emojione.render = config.render; + emoji.packages.emojione.renderPicker = config.renderPicker; + + // RocketChat.emoji.list is the collection of emojis from all emoji packages + for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { + currentEmoji.emojiPackage = 'emojione'; + emoji.list[key] = currentEmoji; + + if (currentEmoji.shortnames) { + currentEmoji.shortnames.forEach((shortname: string) => { + emoji.list[shortname] = currentEmoji; + }); + } + } + } + useEffect(() => { + if (emoji.packages.emojione) { + // Additional settings -- ascii emojis + const ascii = async (): Promise => { + if ((await isSetNotNull(() => emoji.packages.emojione)) && emoji.packages.emojione) { + if (typeof convertAsciiToEmoji === 'boolean') { + emoji.packages.emojione.ascii = convertAsciiToEmoji; + } else { + emoji.packages.emojione.ascii = true; + } + } + }; + + void ascii(); + } + }, [convertAsciiToEmoji]); +}; diff --git a/apps/meteor/app/emoji-emojione/client/index.ts b/apps/meteor/app/emoji-emojione/client/index.ts index 431fd4fbd05c0..2e84171259c48 100644 --- a/apps/meteor/app/emoji-emojione/client/index.ts +++ b/apps/meteor/app/emoji-emojione/client/index.ts @@ -1,2 +1 @@ -import './lib'; import './emojione-sprites.css'; diff --git a/apps/meteor/app/emoji-emojione/client/lib.ts b/apps/meteor/app/emoji-emojione/client/lib.ts deleted file mode 100644 index 53d8a2e1ea8ba..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/lib.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { emoji } from '../../emoji/client'; -import { getUserPreference } from '../../utils/client'; -import { getEmojiConfig } from '../lib/getEmojiConfig'; -import { isSetNotNull } from '../lib/isSetNotNull'; - -const config = getEmojiConfig(); - -emoji.packages.emojione = config.emojione as any; -if (emoji.packages.emojione) { - emoji.packages.emojione.sprites = config.sprites; - emoji.packages.emojione.emojisByCategory = config.emojisByCategory; - emoji.packages.emojione.emojiCategories = config.emojiCategories as any; - emoji.packages.emojione.toneList = config.toneList; - - emoji.packages.emojione.render = config.render; - emoji.packages.emojione.renderPicker = config.renderPicker; - - // RocketChat.emoji.list is the collection of emojis from all emoji packages - for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { - currentEmoji.emojiPackage = 'emojione'; - emoji.list[key] = currentEmoji; - - if (currentEmoji.shortnames) { - currentEmoji.shortnames.forEach((shortname: string) => { - emoji.list[shortname] = currentEmoji; - }); - } - } - - // Additional settings -- ascii emojis - Meteor.startup(() => { - Tracker.autorun(async () => { - if ((await isSetNotNull(() => emoji.packages.emojione)) && emoji.packages.emojione) { - if (await isSetNotNull(() => getUserPreference(Meteor.userId() as string, 'convertAsciiEmoji'))) { - emoji.packages.emojione.ascii = await getUserPreference(Meteor.userId() as string, 'convertAsciiEmoji'); - } else { - emoji.packages.emojione.ascii = true; - } - } - }); - }); -} diff --git a/apps/meteor/app/federation/server/functions/dashboard.js b/apps/meteor/app/federation/server/functions/dashboard.js index 2fd54984b0779..bc9812f4de949 100644 --- a/apps/meteor/app/federation/server/functions/dashboard.js +++ b/apps/meteor/app/federation/server/functions/dashboard.js @@ -2,9 +2,9 @@ import { FederationServers, FederationRoomEvents, Users } from '@rocket.chat/mod import { Meteor } from 'meteor/meteor'; export async function getStatistics() { - const numberOfEvents = await FederationRoomEvents.col.estimatedDocumentCount(); + const numberOfEvents = await FederationRoomEvents.estimatedDocumentCount(); const numberOfFederatedUsers = await Users.countRemote(); - const numberOfServers = await FederationServers.col.estimatedDocumentCount(); + const numberOfServers = await FederationServers.estimatedDocumentCount(); return { numberOfEvents, numberOfFederatedUsers, numberOfServers }; } diff --git a/apps/meteor/app/github-enterprise/client/lib.ts b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts similarity index 64% rename from apps/meteor/app/github-enterprise/client/lib.ts rename to apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts index 97b9e68677992..f61af7bde79e2 100644 --- a/apps/meteor/app/github-enterprise/client/lib.ts +++ b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts @@ -1,9 +1,8 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; -import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; -import { settings } from '../../settings/client'; +import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth'; // GitHub Enterprise Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/github_enterprise // In RocketChat -> Administration the URL needs to be http(s)://{github.enterprise.server}/ @@ -20,11 +19,14 @@ const config: OauthConfig = { }; const GitHubEnterprise = new CustomOAuth('github_enterprise', config); -Meteor.startup(() => { - Tracker.autorun(() => { - if (settings.get('API_GitHub_Enterprise_URL')) { - config.serverURL = settings.get('API_GitHub_Enterprise_URL'); + +export const useGitHubEnterpriseAuth = () => { + const githubApiUrl = useSetting('API_GitHub_Enterprise_URL') as string; + + useEffect(() => { + if (githubApiUrl) { + config.serverURL = githubApiUrl; GitHubEnterprise.configure(config); } - }); -}); + }, [githubApiUrl]); +}; diff --git a/apps/meteor/app/github-enterprise/client/index.ts b/apps/meteor/app/github-enterprise/client/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/github-enterprise/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index 5a2c750c93f9c..80a88b8757c9f 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -240,24 +240,6 @@ function integrationInfoRest() { class WebHookAPI extends APIClass { async authenticatedRoute(request) { - const payloadKeys = Object.keys(request.body); - const payloadIsWrapped = request.body && request.body.payload && payloadKeys.length === 1; - if (payloadIsWrapped && request.headers['content-type'] === 'application/x-www-form-urlencoded') { - try { - request.body = JSON.parse(request.body.payload); - } catch ({ message }) { - return { - error: { - statusCode: 400, - body: { - success: false, - error: message, - }, - }, - }; - } - } - request.integration = await Integrations.findOne({ _id: request.params.integrationId, token: decodeURIComponent(request.params.token), @@ -329,52 +311,27 @@ class WebHookAPI extends APIClass { const Api = new WebHookAPI({ enableCors: true, apiPath: 'hooks/', - auth: { - async user() { - const payloadKeys = Object.keys(this.bodyParams); - const payloadIsWrapped = this.bodyParams && this.bodyParams.payload && payloadKeys.length === 1; - if (payloadIsWrapped && this.request.headers['content-type'] === 'application/x-www-form-urlencoded') { - try { - this.bodyParams = JSON.parse(this.bodyParams.payload); - } catch ({ message }) { - return { - error: { - statusCode: 400, - body: { - success: false, - error: message, - }, - }, - }; - } - } - - this.request.integration = await Integrations.findOne({ - _id: this.request.params.integrationId, - token: decodeURIComponent(this.request.params.token), - }); - - if (!this.request.integration) { - incomingLogger.info(`Invalid integration id ${this.request.params.integrationId} or token ${this.request.params.token}`); +}); - return { - error: { - statusCode: 404, - body: { - success: false, - error: 'Invalid integration id or token provided.', - }, - }, - }; - } +// middleware for special requests that are urlencoded but have a json payload (like GitHub webhooks) +Api.router.use((req, res, next) => { + if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { + return next(); + } - const user = await Users.findOne({ - _id: this.request.integration.userId, - }); + const payloadKeys = Object.keys(req.body); + if (payloadKeys.length !== 1) { + return next(); + } - return { user }; - }, - }, + try { + // need to compose the full payload in this weird way because body-parser thought it was a form + req.bodyParams = JSON.parse(payloadKeys[0] + req.body[payloadKeys[0]]); + return next(); + } catch (e) { + res.writeHead(400); + res.end(JSON.stringify({ success: false, error: e.message })); + } }); Api.addRoute( diff --git a/apps/meteor/app/lib/server/functions/attachMessage.ts b/apps/meteor/app/lib/server/functions/attachMessage.ts index d7bd45ba01b4b..ce5f3aaf7c84d 100644 --- a/apps/meteor/app/lib/server/functions/attachMessage.ts +++ b/apps/meteor/app/lib/server/functions/attachMessage.ts @@ -1,6 +1,6 @@ +import { getUserDisplayName } from '@rocket.chat/core-typings'; import type { IMessage, IRoom, MessageAttachment } from '@rocket.chat/core-typings'; -import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { settings } from '../../../settings/server/cached'; import { getUserAvatarURL } from '../../../utils/server/getUserAvatarURL'; diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index 56b4b48ba29f4..339497b23b8de 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -2,7 +2,7 @@ import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes'; export const closeLivechatRoom = async ( @@ -65,7 +65,7 @@ export const closeLivechatRoom = async ( }; if (forceClose) { - return Livechat.closeRoom({ + return closeRoom({ room, user, options, @@ -78,7 +78,7 @@ export const closeLivechatRoom = async ( throw new Error('error-room-already-closed'); } - return Livechat.closeRoom({ + return closeRoom({ room, user, options, diff --git a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts index fc464f762a980..1c37a0ec84c55 100644 --- a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts +++ b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts @@ -3,7 +3,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; -import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { settings } from '../../../settings/server'; type SubscribedRooms = { @@ -13,7 +13,7 @@ type SubscribedRooms = { export const closeOmnichannelConversations = async (user: IUser, subscribedRooms: SubscribedRooms[]): Promise => { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - const roomsInfo = await LivechatRooms.findByIds( + const roomsInfo = LivechatRooms.findByIds( subscribedRooms.map(({ rid }) => rid), {}, extraQuery, @@ -22,8 +22,8 @@ export const closeOmnichannelConversations = async (user: IUser, subscribedRooms const comment = i18n.t('Agent_deactivated', { lng: language }); const promises: Promise[] = []; - await roomsInfo.forEach((room: any) => { - promises.push(Livechat.closeRoom({ user, room, comment })); + await roomsInfo.forEach((room) => { + promises.push(closeRoom({ user, room, comment })); }); await Promise.all(promises); diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index c9b5354623b73..914b3d9a5d937 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -10,8 +10,9 @@ import { Meteor } from 'meteor/meteor'; import { createDirectRoom } from './createDirectRoom'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; +import { calculateRoomRolePriorityFromRoles } from '../../../../lib/roles/calculateRoomRolePriorityFromRoles'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; -import { calculateRoomRolePriorityFromRoles, syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority'; +import { syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; diff --git a/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts b/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts index 9fba9e9246b28..9400096d8bf51 100644 --- a/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts +++ b/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts @@ -68,7 +68,7 @@ export async function generateUsernameSuggestion(user: Pick, highlig return false; } + const leftBoundary = '(?<=^|[^\\p{L}\\p{N}_])'; + const rightBoundary = '(?=$|[^\\p{L}\\p{N}_])'; + return highlights.some((highlight: string) => { - const hl = escapeRegExp(highlight); - const regexp = new RegExp(`(?): Promise { - if (trim(settings.get('Accounts_CustomFields')) !== '') { - validateCustomFields(formData); - return saveCustomFieldsWithoutValidation(userId, formData); + if (trim(settings.get('Accounts_CustomFields')).length === 0) { + return; } + + validateCustomFields(formData); + return saveCustomFieldsWithoutValidation(userId, formData); }; diff --git a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts index 5383048f13bdd..5a4f3a6096a0d 100644 --- a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts +++ b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts @@ -1,49 +1,58 @@ -import type { IUser, DeepWritable } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import type { UpdateFilter } from 'mongodb'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; import { notifyOnSubscriptionChangedByUserIdAndRoomType } from '../lib/notifyListener'; +const getCustomFieldsMeta = function (customFieldsMeta: string) { + try { + return JSON.parse(customFieldsMeta); + } catch (e) { + throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields'); + } +}; export const saveCustomFieldsWithoutValidation = async function (userId: string, formData: Record): Promise { - if (trim(settings.get('Accounts_CustomFields')) !== '') { - let customFieldsMeta; - try { - customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields')); - } catch (e) { - throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields'); - } + const customFieldsSetting = settings.get('Accounts_CustomFields'); + if (!customFieldsSetting || trim(customFieldsSetting).length === 0) { + return; + } + + // configured custom fields in setting + const customFieldsMeta = getCustomFieldsMeta(customFieldsSetting); - const customFields: Record = {}; - Object.keys(customFieldsMeta).forEach((key) => { - customFields[key] = formData[key]; - }); - await Users.setCustomFields(userId, customFields); + const customFields: Record = Object.keys(customFieldsMeta).reduce( + (acc, currentValue) => ({ + ...acc, + [currentValue]: formData[currentValue], + }), + {}, + ); - // Update customFields of all Direct Messages' Rooms for userId - const setCustomFieldsResponse = await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); - if (setCustomFieldsResponse.modifiedCount) { - void notifyOnSubscriptionChangedByUserIdAndRoomType(userId, 'd'); + const updater = Users.getUpdater(); + + updater.set('customFields', customFields); + + // add modified records to updater + Object.keys(customFields).forEach((fieldName) => { + if (!customFieldsMeta[fieldName].modifyRecordField) { + return; } - for await (const fieldName of Object.keys(customFields)) { - if (!customFieldsMeta[fieldName].modifyRecordField) { - return; - } - - const { modifyRecordField } = customFieldsMeta[fieldName]; - const update: DeepWritable> = {}; - if (modifyRecordField.array) { - update.$addToSet = {}; - update.$addToSet[modifyRecordField.field] = customFields[fieldName]; - } else { - update.$set = {}; - update.$set[modifyRecordField.field] = customFields[fieldName]; - } - - await Users.updateOne({ _id: userId }, update); + const { modifyRecordField } = customFieldsMeta[fieldName]; + + if (modifyRecordField.array) { + updater.addToSet(modifyRecordField.field, customFields[fieldName]); + } else { + updater.set(modifyRecordField.field, customFields[fieldName]); } + }); + + await Users.updateFromUpdater({ _id: userId }, updater); + + // Update customFields of all Direct Messages' Rooms for userId + const setCustomFieldsResponse = await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); + if (setCustomFieldsResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserIdAndRoomType(userId, 'd'); } }; diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 828de8451a217..599e75987eda4 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -284,9 +284,9 @@ export const sendMessage = async function (user: any, message: any, room: any, u } 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); + // If the message has a type (system message), we should notify the listener about it + const messageEvent = message.t ? 'IPostSystemMessageSent' : 'IPostMessageSent'; + void Apps.getBridges()?.getListenerBridge().messageEvent(messageEvent, message); } // TODO: is there an opportunity to send returned data to notifyOnMessageChange? diff --git a/apps/meteor/app/lib/server/functions/syncRolePrioritiesForRoomIfRequired.ts b/apps/meteor/app/lib/server/functions/syncRolePrioritiesForRoomIfRequired.ts index e7445d9fac97e..2b814e002805a 100644 --- a/apps/meteor/app/lib/server/functions/syncRolePrioritiesForRoomIfRequired.ts +++ b/apps/meteor/app/lib/server/functions/syncRolePrioritiesForRoomIfRequired.ts @@ -1,10 +1,12 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import { calculateRoomRolePriorityFromRoles } from '../../../../server/lib/roles/syncRoomRolePriority'; +import { calculateRoomRolePriorityFromRoles } from '../../../../lib/roles/calculateRoomRolePriorityFromRoles'; const READ_BATCH_SIZE = 1000; +const SYNC_VERSION = 2; + async function assignRoomRolePrioritiesFromMap(userIdAndRoomRolePrioritiesMap: Map) { const bulk = Users.col.initializeUnorderedBulkOp(); @@ -35,7 +37,7 @@ async function assignRoomRolePrioritiesFromMap(userIdAndRoomRolePrioritiesMap: M export const syncRolePrioritiesForRoomIfRequired = async (rid: IRoom['_id']) => { const userIdAndRoomRolePrioritiesMap = new Map(); - if (await Rooms.hasCreatedRolePrioritiesForRoom(rid)) { + if (await Rooms.hasCreatedRolePrioritiesForRoom(rid, SYNC_VERSION)) { return; } @@ -64,5 +66,5 @@ export const syncRolePrioritiesForRoomIfRequired = async (rid: IRoom['_id']) => // Flush any remaining priorities in the map await assignRoomRolePrioritiesFromMap(userIdAndRoomRolePrioritiesMap); - await Rooms.markRolePrioritesCreatedForRoom(rid); + await Rooms.markRolePrioritesCreatedForRoom(rid, SYNC_VERSION); }; diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 8fe5812dd62a1..e0a2a844a75eb 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -1,11 +1,10 @@ +import { Authorization } from '@rocket.chat/core-services'; import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; +import { Messages, Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { canAccessRoomAsync } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -26,125 +25,151 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async getChannelHistory({ rid, latest, oldest, inclusive, offset = 0, count = 20, unreads, showThreadMessages = true }) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelHistory' }); - } +export const getChannelHistory = async ({ + rid, + fromUserId, + latest, + oldest, + inclusive, + offset = 0, + count = 20, + unreads, + showThreadMessages = true, +}: { + rid: string; + fromUserId: string; + latest?: Date; + oldest?: Date; + inclusive?: boolean; + offset?: number; + count?: number; + unreads?: boolean; + showThreadMessages?: boolean; +}): Promise => { + check(rid, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelHistory' }); + } - const fromUserId = Meteor.userId(); - if (!fromUserId) { - return false; - } + if (!fromUserId) { + return false; + } - const room = await Rooms.findOneById(rid); - if (!room) { - return false; - } + const room = await Rooms.findOneById(rid); + if (!room) { + return false; + } - if (!(await canAccessRoomAsync(room, { _id: fromUserId }))) { - return false; - } + // Make sure they can access the room + if (!(await Authorization.canReadRoom(room, { _id: fromUserId }))) { + return false; + } - // Make sure they can access the room - if ( - room.t === 'c' && - !(await hasPermissionAsync(fromUserId, 'preview-c-room')) && - !(await Subscriptions.findOneByRoomIdAndUserId(rid, fromUserId, { projection: { _id: 1 } })) - ) { - return false; - } + // Ensure latest is always defined. + if (latest === undefined) { + latest = new Date(); + } - // Ensure latest is always defined. - if (latest === undefined) { - latest = new Date(); - } + // Verify oldest is a date if it exists - // Verify oldest is a date if it exists + if (oldest !== undefined && {}.toString.call(oldest) !== '[object Date]') { + throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); + } - if (oldest !== undefined && {}.toString.call(oldest) !== '[object Date]') { - throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); + + const options: Record = { + sort: { + ts: -1, + }, + skip: offset, + limit: count, + }; + + const records = + oldest === undefined + ? await Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes( + rid, + latest, + hiddenMessageTypes, + options, + showThreadMessages, + inclusive, + ).toArray() + : await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + rid, + oldest, + latest, + hiddenMessageTypes, + options, + showThreadMessages, + inclusive, + ).toArray(); + + const messages = await normalizeMessagesForUser(records, fromUserId); + + if (unreads) { + let unreadNotLoaded = 0; + let firstUnread = undefined; + + if (oldest !== undefined) { + const firstMsg = messages[messages.length - 1]; + if (firstMsg !== undefined && firstMsg.ts > oldest) { + const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + rid, + oldest, + firstMsg.ts, + hiddenMessageTypes, + { + limit: 1, + sort: { + ts: 1, + }, + }, + showThreadMessages, + ); + + const totalCursor = await Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes( + rid, + oldest, + firstMsg.ts, + hiddenMessageTypes, + showThreadMessages, + ); + + firstUnread = (await unreadMessages.toArray())[0]; + unreadNotLoaded = totalCursor; + } } - const hiddenSystemMessages = settings.get('Hide_System_Messages'); + return { + messages: messages || [], + firstUnread, + unreadNotLoaded, + }; + } - const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); + return { + messages: messages || [], + }; +}; - const options: Record = { - sort: { - ts: -1, - }, - skip: offset, - limit: count, - }; +Meteor.methods({ + async getChannelHistory({ rid, latest, oldest, inclusive, offset = 0, count = 20, unreads, showThreadMessages = true }) { + check(rid, String); - const records = - oldest === undefined - ? await Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes( - rid, - latest, - hiddenMessageTypes, - options, - showThreadMessages, - inclusive, - ).toArray() - : await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( - rid, - oldest, - latest, - hiddenMessageTypes, - options, - showThreadMessages, - inclusive, - ).toArray(); - - const messages = await normalizeMessagesForUser(records, fromUserId); - - if (unreads) { - let unreadNotLoaded = 0; - let firstUnread = undefined; - - if (oldest !== undefined) { - const firstMsg = messages[messages.length - 1]; - if (firstMsg !== undefined && firstMsg.ts > oldest) { - const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( - rid, - oldest, - firstMsg.ts, - hiddenMessageTypes, - { - limit: 1, - sort: { - ts: 1, - }, - }, - showThreadMessages, - ); - - const totalCursor = await Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes( - rid, - oldest, - firstMsg.ts, - hiddenMessageTypes, - showThreadMessages, - ); - - firstUnread = (await unreadMessages.toArray())[0]; - unreadNotLoaded = totalCursor; - } - } + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelHistory' }); + } - return { - messages: messages || [], - firstUnread, - unreadNotLoaded, - }; + const fromUserId = Meteor.userId(); + if (!fromUserId) { + return false; } - return { - messages: messages || [], - }; + return getChannelHistory({ rid, fromUserId, latest, oldest, inclusive, offset, count, unreads, showThreadMessages }); }, }); diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 68d18cbfeacf0..45d1343f35721 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -1,12 +1,11 @@ import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; -import { isDirectMessageRoom, isEditedMessage, isOmnichannelRoom, isRoomFederated } from '@rocket.chat/core-typings'; +import { isDirectMessageRoom, isEditedMessage, isOmnichannelRoom, isRoomFederated, getUserDisplayName } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { ActionsBlock } from '@rocket.chat/ui-kit'; import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; -import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { isTruthy } from '../../../../lib/isTruthy'; import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts index 0882bae844f6d..89070d83c677f 100644 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts @@ -30,7 +30,7 @@ API.v1.addRoute( async get() { const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields } = await this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName, onhold, queued } = this.queryParams; + const { agents, departmentId, open, tags, roomName, onhold, queued, units } = this.queryParams; const { createdAt, customFields, closedAt } = this.queryParams; const createdAtParam = validateDateParams('createdAt', createdAt); @@ -70,6 +70,7 @@ API.v1.addRoute( customFields: parsedCf, onhold, queued, + units, options: { offset, count, sort, fields }, }), ); diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index 26449dce39631..650948f253187 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -15,6 +15,7 @@ export async function findRooms({ customFields, onhold, queued, + units, options: { offset, count, fields, sort }, }: { agents?: Array; @@ -33,9 +34,10 @@ export async function findRooms({ customFields?: Record; onhold?: string | boolean; queued?: string | boolean; + units?: Array; options: { offset: number; count: number; fields: Record; sort: Record }; }): Promise }>> { - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, units); const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({ agents, roomName, diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index fe8aa43b663b0..6162380721b59 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -24,6 +24,7 @@ import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivecha import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { closeRoom } from '../../lib/closeRoom'; import type { CloseRoomParams } from '../../lib/localTypes'; import { livechatLogger } from '../../lib/logger'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; @@ -180,7 +181,7 @@ API.v1.addRoute( } } - await LivechatTyped.closeRoom({ visitor, room, comment, options }); + await closeRoom({ visitor, room, comment, options }); return API.v1.success({ rid, comment }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.ts b/apps/meteor/app/livechat/server/api/v1/transcript.ts index e46e841628f13..dee730df84636 100644 --- a/apps/meteor/app/livechat/server/api/v1/transcript.ts +++ b/apps/meteor/app/livechat/server/api/v1/transcript.ts @@ -5,8 +5,7 @@ import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams } import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/LivechatTyped'; -import { sendTranscript } from '../../lib/sendTranscript'; +import { sendTranscript, requestTranscript } from '../../lib/sendTranscript'; API.v1.addRoute( 'livechat/transcript', @@ -66,7 +65,7 @@ API.v1.addRoute( throw new Error('error-invalid-user'); } - await Livechat.requestTranscript({ rid, email, subject, user }); + await requestTranscript({ rid, email, subject, user }); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index ed32f0e2d2795..ded29be65f7f3 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,4 +1,4 @@ -import type { ILivechatCustomField, IRoom } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -7,6 +7,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { validateRequiredCustomFields } from '../../lib/validateRequiredCustomFields'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; API.v1.addRoute( @@ -79,28 +80,32 @@ API.v1.addRoute( ); if (customFields && Array.isArray(customFields) && customFields.length > 0) { - const keys = customFields.map((field) => field.key); const errors: string[] = []; + const keys = customFields.map((field) => field.key); + + const livechatCustomFields = await LivechatCustomField.findByScope( + 'visitor', + { projection: { _id: 1, required: 1 } }, + false, + ).toArray(); + validateRequiredCustomFields(keys, livechatCustomFields); + const matchingCustomFields = livechatCustomFields.filter((field) => keys.includes(field._id)); const processedKeys = await Promise.all( - await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { - projection: { _id: 1 }, - }) - .map(async (field) => { - const customField = customFields.find((f) => f.key === field._id); - if (!customField) { - return; - } - - const { key, value, overwrite } = customField; - // TODO: Change this to Bulk update - if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { - errors.push(key); - } - - return key; - }) - .toArray(), + matchingCustomFields.map(async (field) => { + const customField = customFields.find((f) => f.key === field._id); + if (!customField) { + return; + } + + const { key, value, overwrite } = customField; + // TODO: Change this to Bulk update + if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { + errors.push(key); + } + + return key; + }), ); if (processedKeys.length !== keys.length) { diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index 2f535ad190afa..d21b51ce0184d 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -44,7 +44,7 @@ export const openBusinessHourDefault = async (): Promise => { }; export const createDefaultBusinessHourIfNotExists = async (): Promise => { - if ((await LivechatBusinessHours.col.countDocuments({ type: LivechatBusinessHourTypes.DEFAULT })) === 0) { + if ((await LivechatBusinessHours.countDocuments({ type: LivechatBusinessHourTypes.DEFAULT })) === 0) { await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow()); } }; diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 2360e3e1d5944..ca7ca8dfbd820 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -6,13 +6,20 @@ import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; +import { isMessageFromBot } from '../lib/isMessageFromBot'; export async function markRoomResponded( message: IMessage, room: IOmnichannelRoom, roomUpdater: Updater, ): Promise { - if (isSystemMessage(message) || isEditedMessage(message) || isMessageFromVisitor(message)) { + if ( + isSystemMessage(message) || + isEditedMessage(message) || + isMessageFromVisitor(message) || + (settings.get('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message))) + ) { return; } diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index 9553e9fe981b8..ce420afa1cb86 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -3,7 +3,9 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { settings } from '../../../settings/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; +import { isMessageFromBot } from '../lib/isMessageFromBot'; const getMetricValue = (metric: T | undefined, defaultValue: T): T => metric ?? defaultValue; const calculateTimeDifference = (startTime: T, now: Date): number => @@ -73,6 +75,10 @@ callbacks.add( if (isMessageFromVisitor(message)) { LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater); } else { + if (settings.get('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message))) { + return message; + } + const analyticsData = getAnalyticsData(room, new Date()); LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater); } diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 193f6cdbc60ac..f15aa347ef70a 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,8 +1,7 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services'; +import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, - IOmnichannelRoomClosingInfo, IUser, ILivechatVisitor, SelectedAgent, @@ -12,14 +11,13 @@ import type { AtLeast, TransferData, IOmnichannelAgent, - ILivechatInquiryRecord, UserStatus, IOmnichannelRoomInfo, IOmnichannelRoomExtraData, IOmnichannelSource, ILivechatContactVisitorAssociation, } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; import { LivechatDepartment, @@ -38,12 +36,11 @@ import { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { Filter, ClientSession } from 'mongodb'; +import type { Filter } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { client, shouldRetryTransaction } from '../../../../server/database/utils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; @@ -72,8 +69,7 @@ import { RoutingManager } from './RoutingManager'; import { Visitors, type RegisterGuestType } from './Visitors'; import { registerGuestData } from './contacts/registerGuestData'; import { getRequiredDepartment } from './departmentsLib'; -import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor, ILivechatMessage } from './localTypes'; -import { parseTranscriptRequest } from './parseTranscriptRequest'; +import type { ILivechatMessage } from './localTypes'; type AKeyOf = { [K in keyof T]?: T[K]; @@ -99,13 +95,6 @@ type ICRMData = { crmData?: IOmnichannelRoom['crmData']; }; -type ChatCloser = { _id: string; username: string | undefined }; - -const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser => - (params as CloseRoomParamsByUser).user !== undefined; -const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => - (params as CloseRoomParamsByVisitor).visitor !== undefined; - class LivechatClass { logger: Logger; @@ -140,192 +129,6 @@ class LivechatClass { return agentsOnline; } - async closeRoom(params: CloseRoomParams, attempts = 2): Promise { - let newRoom: IOmnichannelRoom; - let chatCloser: ChatCloser; - let removedInquiryObj: ILivechatInquiryRecord | null; - - const session = client.startSession(); - try { - session.startTransaction(); - const { room, closedBy, removedInquiry } = await this.doCloseRoom(params, session); - await session.commitTransaction(); - - newRoom = room; - chatCloser = closedBy; - removedInquiryObj = removedInquiry; - } catch (e) { - this.logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts }); - await session.abortTransaction(); - // Dont propagate transaction errors - if (shouldRetryTransaction(e)) { - if (attempts > 0) { - this.logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`); - return this.closeRoom(params, attempts - 1); - } - - throw new Error('error-room-cannot-be-closed-try-again'); - } - throw e; - } finally { - await session.endSession(); - } - - // Note: when reaching this point, the room has been closed - // Transaction is commited and so these messages can be sent here. - return this.afterRoomClosed(newRoom, chatCloser, removedInquiryObj, params); - } - - async afterRoomClosed( - newRoom: IOmnichannelRoom, - chatCloser: ChatCloser, - inquiry: ILivechatInquiryRecord | null, - params: CloseRoomParams, - ): Promise { - if (!chatCloser) { - // this should never happen - return; - } - // Note: we are okay with these messages being sent outside of the transaction. The process of sending a message - // is huge and involves multiple db calls. Making it transactionable this way would be really hard. - // And passing just _some_ actions to the transaction creates some deadlocks since messages are updated in the afterSaveMessages callbacks. - const transcriptRequested = - !!params.room.transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always')); - this.logger.debug(`Sending closing message to room ${newRoom._id}`); - await Message.saveSystemMessageAndNotifyUser('livechat-close', newRoom._id, params.comment ?? '', chatCloser, { - groupable: false, - transcriptRequested, - ...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }), - }); - - if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) { - await Message.saveSystemMessage('command', newRoom._id, 'promptTranscript', chatCloser); - } - - this.logger.debug(`Running callbacks for room ${newRoom._id}`); - - process.nextTick(() => { - /** - * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed - * in the next major version of the Apps-Engine - */ - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); - }); - - const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined; - const opts = await parseTranscriptRequest(params.room, params.options, visitor); - if (process.env.TEST_MODE) { - await callbacks.run('livechat.closeRoom', { - room: newRoom, - options: opts, - }); - } else { - callbacks.runAsync('livechat.closeRoom', { - room: newRoom, - options: opts, - }); - } - - void notifyOnRoomChangedById(newRoom._id); - if (inquiry) { - void notifyOnLivechatInquiryChanged(inquiry, 'removed'); - } - - this.logger.debug(`Room ${newRoom._id} was closed`); - } - - async doCloseRoom( - params: CloseRoomParams, - session: ClientSession, - ): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> { - const { comment } = params; - const { room, forceClose } = params; - - this.logger.debug({ msg: `Attempting to close room`, roomId: room._id, forceClose }); - if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) { - this.logger.debug(`Room ${room._id} is not open`); - throw new Error('error-room-closed'); - } - - const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation'); - if (commentRequired && !comment?.trim()) { - throw new Error('error-comment-is-required'); - } - - const { updatedOptions: options } = await this.resolveChatTags(room, params.options); - this.logger.debug(`Resolved chat tags for room ${room._id}`); - - const now = new Date(); - const { _id: rid, servedBy } = room; - const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000; - - const closeData: IOmnichannelRoomClosingInfo = { - closedAt: now, - chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000, - ...(serviceTimeDuration && { serviceTimeDuration }), - ...options, - }; - this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`); - - if (isRoomClosedByUserParams(params)) { - const { user } = params; - this.logger.debug(`Closing by user ${user?._id}`); - closeData.closer = 'user'; - closeData.closedBy = { - _id: user?._id || '', - username: user?.username, - }; - } else if (isRoomClosedByVisitorParams(params)) { - const { visitor } = params; - this.logger.debug(`Closing by visitor ${params.visitor._id}`); - closeData.closer = 'visitor'; - closeData.closedBy = { - _id: visitor._id, - username: visitor.username, - }; - } else { - throw new Error('Error: Please provide details of the user or visitor who closed the room'); - } - - this.logger.debug(`Updating DB for room ${room._id} with close data`); - - const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session }); - const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session }); - if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) { - throw new Error('Error removing inquiry'); - } - - const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session }); - if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) { - throw new Error('Error closing room'); - } - - const subs = await Subscriptions.countByRoomId(rid, { session }); - if (subs) { - const removedSubs = await Subscriptions.removeByRoomId(rid, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - session, - }); - - if (!params.forceClose && removedSubs.deletedCount !== subs) { - throw new Error('Error removing subscriptions'); - } - } - - this.logger.debug(`DB updated for room ${room._id}`); - - // Retrieve the closed room - const newRoom = await LivechatRooms.findOneById(rid, { session }); - if (!newRoom) { - throw new Error('Error: Room not found'); - } - - return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry }; - } - private makeVisitorAssociation(visitorId: string, roomInfo: IOmnichannelSource): ILivechatContactVisitorAssociation { return { visitorId, @@ -433,7 +236,7 @@ class LivechatClass { async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise { if (agent?.agentId) { - return Users.checkOnlineAgents(agent.agentId); + return Users.checkOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); } if (department) { @@ -452,7 +255,7 @@ class LivechatClass { return this.checkOnlineAgents(dep?.fallbackForwardDepartment); } - return Users.checkOnlineAgents(); + return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); } async removeRoom(rid: string) { @@ -515,70 +318,6 @@ class LivechatClass { return Users.countBotAgents(); } - private async resolveChatTags( - room: IOmnichannelRoom, - options: CloseRoomParams['options'] = {}, - ): Promise<{ updatedOptions: CloseRoomParams['options'] }> { - this.logger.debug(`Resolving chat tags for room ${room._id}`); - - const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [ - ...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))), - ]; - - const { departmentId, tags: optionsTags } = room; - const { clientAction, tags: oldRoomTags } = options; - const roomTags = concatUnique(oldRoomTags, optionsTags); - - if (!departmentId) { - return { - updatedOptions: { - ...options, - ...(roomTags.length && { tags: roomTags }), - }, - }; - } - - const department = await LivechatDepartment.findOneById>( - departmentId, - { - projection: { requestTagBeforeClosingChat: 1, chatClosingTags: 1 }, - }, - ); - if (!department) { - return { - updatedOptions: { - ...options, - ...(roomTags.length && { tags: roomTags }), - }, - }; - } - - const { requestTagBeforeClosingChat, chatClosingTags } = department; - const extraRoomTags = concatUnique(roomTags, chatClosingTags); - - if (!requestTagBeforeClosingChat) { - return { - updatedOptions: { - ...options, - ...(extraRoomTags.length && { tags: extraRoomTags }), - }, - }; - } - - const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); - const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; - if (!checkRoomTags || !checkDepartmentTags) { - throw new Error('error-tags-must-be-assigned-before-closing-chat'); - } - - return { - updatedOptions: { - ...options, - ...(extraRoomTags.length && { tags: extraRoomTags }), - }, - }; - } - async sendRequest( postData: { type: string; @@ -739,20 +478,6 @@ class LivechatClass { return true; } - async closeOpenChats(userId: string, comment?: string) { - this.logger.debug(`Closing open chats for user ${userId}`); - const user = await Users.findOneById(userId); - - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); - const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); - const promises: Promise[] = []; - await openChats.forEach((room) => { - promises.push(this.closeRoom({ user, room, comment })); - }); - - await Promise.all(promises); - } - async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); if (room.onHold) { @@ -1103,48 +828,6 @@ class LivechatClass { return result.modifiedCount; } - async requestTranscript({ - rid, - email, - subject, - user, - }: { - rid: string; - email: string; - subject: string; - user: AtLeast; - }) { - const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1 } }); - - if (!room?.open) { - throw new Meteor.Error('error-invalid-room', 'Invalid room'); - } - - if (room.transcriptRequest) { - throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested'); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Error('error-mac-limit-reached'); - } - - const { _id, username, name, utcOffset } = user; - const transcriptRequest = { - requestedAt: new Date(), - requestedBy: { - _id, - username, - name, - utcOffset, - }, - email, - subject, - }; - - await LivechatRooms.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest); - return true; - } - async afterRemoveAgent(user: AtLeast) { await callbacks.run('livechat.afterAgentRemoved', { agent: user }); return true; diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 123c56f4d31fa..a62c7dfea46be 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -10,6 +10,7 @@ import type { SelectedAgent, InquiryWithAgentInfo, TransferData, + IUser, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -60,7 +61,7 @@ type Routing = { delegateAgent(agent: SelectedAgent | undefined, inquiry: ILivechatInquiryRecord): Promise; removeAllRoomSubscriptions(room: Pick, ignoreUser?: { _id: string }): Promise; - assignAgent(inquiry: InquiryWithAgentInfo, room: IOmnichannelRoom, agent: SelectedAgent): Promise; + assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise<{ inquiry: InquiryWithAgentInfo; user: IUser }>; }; export const RoutingManager: Routing = { @@ -116,7 +117,7 @@ export const RoutingManager: Routing = { return this.takeInquiry(inquiry, agent, options, room); }, - async assignAgent(inquiry: InquiryWithAgentInfo, room: IOmnichannelRoom, agent: SelectedAgent): Promise { + async assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise<{ inquiry: InquiryWithAgentInfo; user: IUser }> { check( agent, Match.ObjectIncluding({ @@ -137,17 +138,17 @@ export const RoutingManager: Routing = { await Rooms.incUsersCountById(rid, 1); const user = await Users.findOneById(agent.agentId); - - if (user) { - await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); + if (!user) { + throw new Error('error-user-not-found'); } + await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); + await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquiry ${inquiry._id}. Instances notified`); - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); - return inquiry; + return { inquiry, user }; }, async unassignAgent(inquiry, departmentId, shouldQueue = false) { @@ -254,7 +255,7 @@ export const RoutingManager: Routing = { logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); // assignAgent changes the room data to add the agent serving the conversation. afterTakeInquiry expects room object to be updated - const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent); + const { inquiry: returnedInquiry, user } = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); const roomAfterUpdate = await LivechatRooms.findOneById(rid); if (!roomAfterUpdate) { @@ -262,10 +263,11 @@ export const RoutingManager: Routing = { throw new Error('error-room-not-found'); } + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room: roomAfterUpdate, user }); callbacks.runAsync( 'livechat.afterTakeInquiry', { - inquiry: inq, + inquiry: returnedInquiry, room: roomAfterUpdate, }, agent, diff --git a/apps/meteor/app/livechat/server/lib/closeRoom.ts b/apps/meteor/app/livechat/server/lib/closeRoom.ts new file mode 100644 index 0000000000000..3ff105051a090 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/closeRoom.ts @@ -0,0 +1,289 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import { Message } from '@rocket.chat/core-services'; +import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions, Users } from '@rocket.chat/models'; +import type { ClientSession } from 'mongodb'; + +import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes'; +import { livechatLogger as logger } from './logger'; +import { parseTranscriptRequest } from './parseTranscriptRequest'; +import { callbacks } from '../../../../lib/callbacks'; +import { client, shouldRetryTransaction } from '../../../../server/database/utils'; +import { + notifyOnLivechatInquiryChanged, + notifyOnRoomChangedById, + notifyOnSubscriptionChanged, +} from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; + +type ChatCloser = { _id: string; username: string | undefined }; + +const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser => + (params as CloseRoomParamsByUser).user !== undefined; +const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => + (params as CloseRoomParamsByVisitor).visitor !== undefined; + +export async function closeRoom(params: CloseRoomParams, attempts = 2): Promise { + let newRoom: IOmnichannelRoom; + let chatCloser: ChatCloser; + let removedInquiryObj: ILivechatInquiryRecord | null; + + const session = client.startSession(); + try { + session.startTransaction(); + const { room, closedBy, removedInquiry } = await doCloseRoom(params, session); + await session.commitTransaction(); + + newRoom = room; + chatCloser = closedBy; + removedInquiryObj = removedInquiry; + } catch (e) { + logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts }); + await session.abortTransaction(); + // Dont propagate transaction errors + if (shouldRetryTransaction(e)) { + if (attempts > 0) { + logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`); + return closeRoom(params, attempts - 1); + } + + throw new Error('error-room-cannot-be-closed-try-again'); + } + throw e; + } finally { + await session.endSession(); + } + + // Note: when reaching this point, the room has been closed + // Transaction is commited and so these messages can be sent here. + return afterRoomClosed(newRoom, chatCloser, removedInquiryObj, params); +} + +async function afterRoomClosed( + newRoom: IOmnichannelRoom, + chatCloser: ChatCloser, + inquiry: ILivechatInquiryRecord | null, + params: CloseRoomParams, +): Promise { + if (!chatCloser) { + // this should never happen + return; + } + // Note: we are okay with these messages being sent outside of the transaction. The process of sending a message + // is huge and involves multiple db calls. Making it transactionable this way would be really hard. + // And passing just _some_ actions to the transaction creates some deadlocks since messages are updated in the afterSaveMessages callbacks. + const transcriptRequested = + !!params.room.transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always')); + logger.debug(`Sending closing message to room ${newRoom._id}`); + await Message.saveSystemMessageAndNotifyUser('livechat-close', newRoom._id, params.comment ?? '', chatCloser, { + groupable: false, + transcriptRequested, + ...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }), + }); + + if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) { + await Message.saveSystemMessage('command', newRoom._id, 'promptTranscript', chatCloser); + } + + logger.debug(`Running callbacks for room ${newRoom._id}`); + + process.nextTick(() => { + /** + * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed + * in the next major version of the Apps-Engine + */ + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); + }); + + const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined; + const opts = await parseTranscriptRequest(params.room, params.options, visitor); + if (process.env.TEST_MODE) { + await callbacks.run('livechat.closeRoom', { + room: newRoom, + options: opts, + }); + } else { + callbacks.runAsync('livechat.closeRoom', { + room: newRoom, + options: opts, + }); + } + + void notifyOnRoomChangedById(newRoom._id); + if (inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } + + logger.debug(`Room ${newRoom._id} was closed`); +} + +async function doCloseRoom( + params: CloseRoomParams, + session: ClientSession, +): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> { + const { comment } = params; + const { room, forceClose } = params; + + logger.debug({ msg: `Attempting to close room`, roomId: room._id, forceClose }); + if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) { + logger.debug(`Room ${room._id} is not open`); + throw new Error('error-room-closed'); + } + + const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation'); + if (commentRequired && !comment?.trim()) { + throw new Error('error-comment-is-required'); + } + + const { updatedOptions: options } = await resolveChatTags(room, params.options); + logger.debug(`Resolved chat tags for room ${room._id}`); + + const now = new Date(); + const { _id: rid, servedBy } = room; + const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000; + + const closeData: IOmnichannelRoomClosingInfo = { + closedAt: now, + chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000, + ...(serviceTimeDuration && { serviceTimeDuration }), + ...options, + }; + logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`); + + if (isRoomClosedByUserParams(params)) { + const { user } = params; + logger.debug(`Closing by user ${user?._id}`); + closeData.closer = 'user'; + closeData.closedBy = { + _id: user?._id || '', + username: user?.username, + }; + } else if (isRoomClosedByVisitorParams(params)) { + const { visitor } = params; + logger.debug(`Closing by visitor ${params.visitor._id}`); + closeData.closer = 'visitor'; + closeData.closedBy = { + _id: visitor._id, + username: visitor.username, + }; + } else { + throw new Error('Error: Please provide details of the user or visitor who closed the room'); + } + + logger.debug(`Updating DB for room ${room._id} with close data`); + + const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session }); + const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session }); + if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) { + throw new Error('Error removing inquiry'); + } + + const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session }); + if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) { + throw new Error('Error closing room'); + } + + const subs = await Subscriptions.countByRoomId(rid, { session }); + if (subs) { + const removedSubs = await Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + session, + }); + + if (!params.forceClose && removedSubs.deletedCount !== subs) { + throw new Error('Error removing subscriptions'); + } + } + + logger.debug(`DB updated for room ${room._id}`); + + // Retrieve the closed room + const newRoom = await LivechatRooms.findOneById(rid, { session }); + if (!newRoom) { + throw new Error('Error: Room not found'); + } + + return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry }; +} + +async function resolveChatTags( + room: IOmnichannelRoom, + options: CloseRoomParams['options'] = {}, +): Promise<{ updatedOptions: CloseRoomParams['options'] }> { + logger.debug(`Resolving chat tags for room ${room._id}`); + + const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [ + ...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))), + ]; + + const { departmentId, tags: optionsTags } = room; + const { clientAction, tags: oldRoomTags } = options; + const roomTags = concatUnique(oldRoomTags, optionsTags); + + if (!departmentId) { + return { + updatedOptions: { + ...options, + ...(roomTags.length && { tags: roomTags }), + }, + }; + } + + const department = await LivechatDepartment.findOneById>( + departmentId, + { + projection: { requestTagBeforeClosingChat: 1, chatClosingTags: 1 }, + }, + ); + if (!department) { + return { + updatedOptions: { + ...options, + ...(roomTags.length && { tags: roomTags }), + }, + }; + } + + const { requestTagBeforeClosingChat, chatClosingTags } = department; + const extraRoomTags = concatUnique(roomTags, chatClosingTags); + + if (!requestTagBeforeClosingChat) { + return { + updatedOptions: { + ...options, + ...(extraRoomTags.length && { tags: extraRoomTags }), + }, + }; + } + + const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); + const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; + if (!checkRoomTags || !checkDepartmentTags) { + throw new Error('error-tags-must-be-assigned-before-closing-chat'); + } + + return { + updatedOptions: { + ...options, + ...(extraRoomTags.length && { tags: extraRoomTags }), + }, + }; +} + +export async function closeOpenChats(userId: string, comment?: string) { + logger.debug(`Closing open chats for user ${userId}`); + const user = await Users.findOneById(userId); + + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); + const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); + const promises: Promise[] = []; + await openChats.forEach((room) => { + promises.push(closeRoom({ user, room, comment })); + }); + + await Promise.all(promises); +} diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts index be92a7cfcc545..69b89cdc1c907 100644 --- a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts +++ b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts @@ -2,9 +2,11 @@ import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; import type { FindCursor } from 'mongodb'; +import { settings } from '../../../settings/server'; + export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId); + return Users.findOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); } if (department) { @@ -20,5 +22,5 @@ export async function getOnlineAgents(department?: string, agent?: SelectedAgent return Users.findByIds([...new Set(agentIds)]); } - return Users.findOnlineAgents(); + return Users.findOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); } diff --git a/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts new file mode 100644 index 0000000000000..8c53bf7c555d0 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts @@ -0,0 +1,6 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +export async function isMessageFromBot(message: IMessage): Promise { + return Users.isUserInRole(message.u._id, 'bot'); +} diff --git a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts index f526280c757b4..ee3385879cc4c 100644 --- a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts +++ b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts @@ -38,7 +38,7 @@ class AutoSelection implements IRoutingMethod { ); } - return Users.getNextAgent(ignoreAgentId, extraQuery); + return Users.getNextAgent(ignoreAgentId, extraQuery, settings.get('Livechat_enabled_when_agent_idle')); } } diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index e701e8f5b8639..9d4af9f809730 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, Omnichannel } from '@rocket.chat/core-services'; import { type IUser, type MessageTypesValues, @@ -6,6 +6,7 @@ import { type ILivechatVisitor, isFileAttachment, isFileImageAttachment, + type AtLeast, } from '@rocket.chat/core-typings'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { Logger } from '@rocket.chat/logger'; @@ -222,3 +223,45 @@ export async function sendTranscript({ return true; } + +export async function requestTranscript({ + rid, + email, + subject, + user, +}: { + rid: string; + email: string; + subject: string; + user: AtLeast; +}) { + const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1 } }); + + if (!room?.open) { + throw new Meteor.Error('error-invalid-room', 'Invalid room'); + } + + if (room.transcriptRequest) { + throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested'); + } + + if (!(await Omnichannel.isWithinMACLimit(room))) { + throw new Error('error-mac-limit-reached'); + } + + const { _id, username, name, utcOffset } = user; + const transcriptRequest = { + requestedAt: new Date(), + requestedBy: { + _id, + username, + name, + utcOffset, + }, + email, + subject, + }; + + await LivechatRooms.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest); + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index 5ddd25e90bd2e..71bc21f600323 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -2,6 +2,7 @@ import { Logger } from '@rocket.chat/logger'; import { settings } from '../../../../settings/server'; import { Livechat } from '../LivechatTyped'; +import { closeOpenChats } from '../closeRoom'; const logger = new Logger('AgentStatusWatcher'); @@ -68,7 +69,7 @@ export const onlineAgents = { try { if (action === 'close') { - return await Livechat.closeOpenChats(userId, comment); + return await closeOpenChats(userId, comment); } if (action === 'forward') { diff --git a/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts b/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts new file mode 100644 index 0000000000000..008a4b13c7824 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts @@ -0,0 +1,16 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; + +export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { + const errors: string[] = []; + const requiredCustomFields = livechatCustomFields.filter((field) => field.required); + + requiredCustomFields.forEach((field) => { + if (!customFields.find((f) => f === field._id)) { + errors.push(field._id); + } + }); + + if (errors.length > 0) { + throw new Error(`Missing required custom fields: ${errors.join(', ')}`); + } +}; diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.ts b/apps/meteor/app/livechat/server/methods/closeRoom.ts index 19c8b27093893..4d6daa5001cd8 100644 --- a/apps/meteor/app/livechat/server/methods/closeRoom.ts +++ b/apps/meteor/app/livechat/server/methods/closeRoom.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { Livechat } from '../lib/LivechatTyped'; +import { closeRoom } from '../lib/closeRoom'; type CloseRoomOptions = { clientAction?: boolean; @@ -87,7 +87,7 @@ Meteor.methods({ }); } - await Livechat.closeRoom({ + await closeRoom({ user, room, comment, diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index 69a4444e8394e..407ba73806864 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -13,75 +13,78 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async unreadMessages(firstUnreadMessage, room) { - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { +export const unreadMessages = async (userId: string, firstUnreadMessage?: IMessage, room?: IRoom['_id']): Promise => { + if (room && typeof room === 'string') { + const lastMessage = ( + await Messages.findVisibleByRoomId(room, { + limit: 1, + sort: { ts: -1 }, + }).toArray() + )[0]; + + if (!lastMessage) { + throw new Meteor.Error('error-no-message-for-unread', 'There are no messages to mark unread', { method: 'unreadMessages', + action: 'Unread_messages', }); } - if (room && typeof room === 'string') { - const lastMessage = ( - await Messages.findVisibleByRoomId(room, { - limit: 1, - sort: { ts: -1 }, - }).toArray() - )[0]; + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(lastMessage.rid, userId); + } - if (!lastMessage) { - throw new Meteor.Error('error-no-message-for-unread', 'There are no messages to mark unread', { - method: 'unreadMessages', - action: 'Unread_messages', - }); - } + return; + } - const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); - if (setAsUnreadResponse.modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(lastMessage.rid, userId); - } + if (typeof firstUnreadMessage?._id !== 'string') { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'unreadMessages', + action: 'Unread_messages', + }); + } - return; - } + const originalMessage = await Messages.findOneById(firstUnreadMessage._id, { + projection: { + u: 1, + rid: 1, + ts: 1, + }, + }); + if (!originalMessage || userId === originalMessage.u._id) { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'unreadMessages', + action: 'Unread_messages', + }); + } + const lastSeen = (await Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, userId))?.ls; + if (!lastSeen) { + throw new Meteor.Error('error-subscription-not-found', 'Subscription not found', { + method: 'unreadMessages', + action: 'Unread_messages', + }); + } - if (typeof firstUnreadMessage?._id !== 'string') { - throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { - method: 'unreadMessages', - action: 'Unread_messages', - }); - } + if (firstUnreadMessage.ts >= lastSeen) { + return logger.debug('Provided message is already marked as unread'); + } - const originalMessage = await Messages.findOneById(firstUnreadMessage._id, { - projection: { - u: 1, - rid: 1, - file: 1, - ts: 1, - }, - }); - if (!originalMessage || userId === originalMessage.u._id) { - throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { - method: 'unreadMessages', - action: 'Unread_messages', - }); - } - const lastSeen = (await Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, userId))?.ls; - if (!lastSeen) { - throw new Meteor.Error('error-subscription-not-found', 'Subscription not found', { + logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(originalMessage.rid, userId); + } +}; + +Meteor.methods({ + async unreadMessages(firstUnreadMessage, room) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unreadMessages', - action: 'Unread_messages', }); } - if (firstUnreadMessage.ts >= lastSeen) { - return logger.debug('Provided message is already marked as unread'); - } - - logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); - const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); - if (setAsUnreadResponse.modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(originalMessage.rid, userId); - } + return unreadMessages(userId, firstUnreadMessage, room); }, }); diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts index ab21aa3a8cf33..76e1a8f94b86b 100644 --- a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts +++ b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts @@ -73,4 +73,4 @@ API.v1.addAuthMethod(async function () { return oAuth2ServerAuth(this.request); }); -(WebApp.connectHandlers as ReturnType).use(oauth2server.app); +(WebApp.connectHandlers as unknown as ReturnType).use(oauth2server.app); diff --git a/apps/meteor/app/push/server/gcm.ts b/apps/meteor/app/push/server/gcm.ts deleted file mode 100644 index 192a08ed666e5..0000000000000 --- a/apps/meteor/app/push/server/gcm.ts +++ /dev/null @@ -1,103 +0,0 @@ -import EJSON from 'ejson'; -import gcm from 'node-gcm'; - -import { logger } from './logger'; -import type { NativeNotificationParameters } from './push'; - -/** - * @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]; - } - - // Check if any tokens in there to send - if (!userTokens.length) { - logger.debug('sendGCM no push tokens found'); - return; - } - - logger.debug('sendGCM', userTokens, notification); - - // Allow user to set payload - const data: Record = notification.payload ? { ejson: EJSON.stringify(notification.payload) } : {}; - - data.title = notification.title; - data.message = notification.text; - - // Set image - if (notification.gcm?.image != null) { - data.image = notification.gcm?.image; - } - - // Set extra details - if (notification.badge != null) { - data.msgcnt = notification.badge; - } - if (notification.sound != null) { - data.soundname = notification.sound; - } - if (notification.notId != null) { - data.notId = notification.notId; - } - if (notification.gcm?.style != null) { - data.style = notification.gcm?.style; - } - - if (notification.contentAvailable != null) { - data['content-available'] = notification.contentAvailable; - } - - const message = new gcm.Message({ - collapseKey: notification.from, - // Requires delivery of real-time messages to users while device is in Doze or app is in App Standby. - // https://developer.android.com/training/monitoring-device-state/doze-standby#exemption-cases - priority: 'high', - // delayWhileIdle: true, - // timeToLive: 4, - // restricted_package_name: 'dk.gi2.app' - data, - }); - - logger.debug(`Create GCM Sender using "${options.gcm.apiKey}"`); - const sender = new gcm.Sender(options.gcm.apiKey); - - userTokens.forEach((value) => logger.debug(`A:Send message to: ${value}`)); - - const userToken = userTokens.length === 1 ? userTokens[0] : null; - - sender.send(message, userTokens, 5, (err, result) => { - if (err) { - logger.debug({ msg: 'ANDROID ERROR: result of sender', result }); - return; - } - - if (result === null) { - logger.debug('ANDROID: Result of sender is null'); - return; - } - - logger.debug({ msg: 'ANDROID: Result of sender', result }); - - if (result.canonical_ids === 1 && userToken && result.results?.[0].registration_id) { - // This is an old device, token is replaced - try { - _replaceToken({ gcm: userToken }, { gcm: result.results[0].registration_id }); - } catch (err) { - logger.error({ msg: 'Error replacing token', err }); - } - } - // We cant send to that token - might not be registered - // ask the user to remove the token from the list - if (result.failure !== 0 && userToken) { - // This is an old device, token is replaced - try { - _removeToken({ gcm: userToken }); - } catch (err) { - logger.error({ msg: 'Error removing token', err }); - } - } - }); -}; diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 5c664aa966272..95543aaa43e90 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -10,7 +10,6 @@ import { Meteor } from 'meteor/meteor'; import { initAPN, sendAPN } from './apn'; import type { PushOptions, PendingPushNotification } from './definition'; import { sendFCM } from './fcm'; -import { sendGCM } from './gcm'; import { logger } from './logger'; import { settings } from '../../settings/server'; @@ -197,40 +196,24 @@ class PushClass { } else if ('gcm' in app.token && app.token.gcm) { countGcm.push(app._id); - // Send to GCM - // We do support multiple here - so we should construct an array - // and send it bulk - Investigate limit count of id's - // 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, - _replaceToken: this.replaceToken, - _removeToken: this.removeToken, - options: this.options as RequiredField, - }); - } + // 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 { throw new Error('send got a faulty query'); } @@ -408,7 +391,7 @@ class PushClass { // Add some verbosity about the send result, making sure the developer // understands what just happened. if (!countApn.length && !countGcm.length) { - if ((await AppsTokens.col.estimatedDocumentCount()) === 0) { + if ((await AppsTokens.estimatedDocumentCount()) === 0) { logger.debug('GUIDE: The "AppsTokens" is empty - No clients have registered on the server yet...'); } } else if (!countApn.length) { diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 12f24cd3bc10c..c37be6421091d 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -109,14 +109,14 @@ export const statistics = { } // User statistics - statistics.totalUsers = await Users.col.countDocuments({}); + statistics.totalUsers = await Users.estimatedDocumentCount(); statistics.activeUsers = await Users.getActiveLocalUserCount(); statistics.activeGuests = await Users.getActiveLocalGuestCount(); - statistics.nonActiveUsers = await Users.col.countDocuments({ active: false }); - statistics.appUsers = await Users.col.countDocuments({ type: 'app' }); - statistics.onlineUsers = await Users.col.countDocuments({ status: UserStatus.ONLINE }); - statistics.awayUsers = await Users.col.countDocuments({ status: UserStatus.AWAY }); - statistics.busyUsers = await Users.col.countDocuments({ status: UserStatus.BUSY }); + statistics.nonActiveUsers = await Users.countDocuments({ active: false }); + statistics.appUsers = await Users.countDocuments({ type: 'app' }); + statistics.onlineUsers = await Users.countDocuments({ status: UserStatus.ONLINE }); + statistics.awayUsers = await Users.countDocuments({ status: UserStatus.AWAY }); + statistics.busyUsers = await Users.countDocuments({ status: UserStatus.BUSY }); statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers - statistics.busyUsers; statsPms.push( @@ -126,7 +126,7 @@ export const statistics = { ); // Room statistics - statistics.totalRooms = await Rooms.col.countDocuments({}); + statistics.totalRooms = await Rooms.estimatedDocumentCount(); statistics.totalChannels = await Rooms.countByType('c'); statistics.totalPrivateGroups = await Rooms.countByType('p'); statistics.totalDirect = await Rooms.countByType('d'); @@ -190,7 +190,7 @@ export const statistics = { ); // Number of custom fields - statsPms.push((statistics.totalCustomFields = await LivechatCustomField.countDocuments({}))); + statsPms.push((statistics.totalCustomFields = await LivechatCustomField.estimatedDocumentCount())); // Number of public custom fields statsPms.push((statistics.totalLivechatPublicCustomFields = await LivechatCustomField.countDocuments({ public: true }))); @@ -256,7 +256,7 @@ export const statistics = { // Amount of VoIP Extensions connected statsPms.push( - Users.col.countDocuments({ extension: { $exists: true } }).then((count) => { + Users.countDocuments({ extension: { $exists: true } }).then((count) => { statistics.voipExtensions = count; }), ); @@ -313,25 +313,25 @@ export const statistics = { // Message statistics const channels = await Rooms.findByType('c', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalChannelDiscussionsMessages = await channels.reduce(function _countChannelDiscussionsMessages(num: number, room: IRoom) { + const totalChannelDiscussionsMessages = channels.reduce(function _countChannelDiscussionsMessages(num: number, room: IRoom) { return num + (room.prid ? room.msgs : 0); }, 0); statistics.totalChannelMessages = - (await channels.reduce(function _countChannelMessages(num: number, room: IRoom) { + channels.reduce(function _countChannelMessages(num: number, room: IRoom) { return num + room.msgs; - }, 0)) - totalChannelDiscussionsMessages; + }, 0) - totalChannelDiscussionsMessages; const privateGroups = await Rooms.findByType('p', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalPrivateGroupsDiscussionsMessages = await privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages( + const totalPrivateGroupsDiscussionsMessages = privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages( num: number, room: IRoom, ) { return num + (room.prid ? room.msgs : 0); }, 0); statistics.totalPrivateGroupMessages = - (await privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { + privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { return num + room.msgs; - }, 0)) - totalPrivateGroupsDiscussionsMessages; + }, 0) - totalPrivateGroupsDiscussionsMessages; statistics.totalDiscussionsMessages = totalPrivateGroupsDiscussionsMessages + totalChannelDiscussionsMessages; @@ -394,7 +394,7 @@ export const statistics = { statistics.enterpriseReady = true; statsPms.push( - Uploads.col.estimatedDocumentCount().then((count) => { + Uploads.estimatedDocumentCount().then((count) => { statistics.uploadsTotal = count; }), ); @@ -417,7 +417,7 @@ export const statistics = { statistics.migration = await getControl(); statsPms.push( - InstanceStatus.col.countDocuments({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).then((count) => { + InstanceStatus.countDocuments({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).then((count) => { statistics.instanceCount = count; }), ); diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 0682dc05eb574..5229f32842d26 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.4.0-develop" + "version": "7.5.0-develop" } diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenuHeader.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenuHeader.tsx index 158b666b64c1e..9c506cf350f47 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenuHeader.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenuHeader.tsx @@ -1,12 +1,12 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box, Margins } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useUserDisplayName } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; import MarkdownText from '../../../components/MarkdownText'; import { UserStatus } from '../../../components/UserStatus'; -import { useUserDisplayName } from '../../../hooks/useUserDisplayName'; type UserMenuHeaderProps = { user: IUser }; diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index ce5cf7765c936..fdfb800c01684 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -1,7 +1,7 @@ -import { CheckOption, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; +import { CheckOption, Option, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, ReactElement } from 'react'; import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,6 +15,7 @@ type AutoCompleteDepartmentMultipleProps = { onlyMyDepartments?: boolean; showArchived?: boolean; enabled?: boolean; + withCheckbox?: boolean; } & Omit, 'options'>; const AutoCompleteDepartmentMultiple = ({ @@ -22,6 +23,7 @@ const AutoCompleteDepartmentMultiple = ({ onlyMyDepartments = false, showArchived = false, enabled = false, + withCheckbox = true, onChange = () => undefined, }: AutoCompleteDepartmentMultipleProps) => { const { t } = useTranslation(); @@ -43,6 +45,18 @@ const AutoCompleteDepartmentMultiple = ({ return [...departmentsItems, ...pending]; }, [departmentsItems, value]); + const renderItem = ({ label, ...props }: ComponentProps): ReactElement => { + if (withCheckbox) { + {label}} + selected={value.some((item) => item.value === props.value)} + />; + } + + return