diff --git a/.changeset/angry-baboons-trade.md b/.changeset/angry-baboons-trade.md new file mode 100644 index 0000000000000..6b78acc776e24 --- /dev/null +++ b/.changeset/angry-baboons-trade.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-kit': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue that prevented BlockAction interactions from having room information when triggered in a ContextualBar surface diff --git a/.changeset/beige-parrots-lead.md b/.changeset/beige-parrots-lead.md new file mode 100644 index 0000000000000..fdb02b3ec9647 --- /dev/null +++ b/.changeset/beige-parrots-lead.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Swap usage of internal @rocket.chat/apps-engine internal APIs to @rocket.chat/apps package diff --git a/.changeset/big-corners-tie.md b/.changeset/big-corners-tie.md new file mode 100644 index 0000000000000..d94dbbfb5daca --- /dev/null +++ b/.changeset/big-corners-tie.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Ensures OAuth tokens are cleaned up after user deactivation diff --git a/.changeset/brave-fans-tie.md b/.changeset/brave-fans-tie.md new file mode 100644 index 0000000000000..3d2fa6b1b02af --- /dev/null +++ b/.changeset/brave-fans-tie.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds 4 new permissions (assigned to admins by default) to control the visibility of each tab inside the ABAC Administration panel diff --git a/.changeset/brown-files-work.md b/.changeset/brown-files-work.md new file mode 100644 index 0000000000000..9459cee560b56 --- /dev/null +++ b/.changeset/brown-files-work.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': minor +--- + +Remove files that weren't accessible to apps at development time diff --git a/.changeset/bump-patch-1779457640608.md b/.changeset/bump-patch-1779457640608.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1779457640608.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1780029980454.md b/.changeset/bump-patch-1780029980454.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1780029980454.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1780545712119.md b/.changeset/bump-patch-1780545712119.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1780545712119.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1780685193955.md b/.changeset/bump-patch-1780685193955.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1780685193955.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1781098424278.md b/.changeset/bump-patch-1781098424278.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1781098424278.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1781123233262.md b/.changeset/bump-patch-1781123233262.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1781123233262.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/calm-seas-run.md b/.changeset/calm-seas-run.md new file mode 100644 index 0000000000000..fe74a2e4fc672 --- /dev/null +++ b/.changeset/calm-seas-run.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue that allowed a room converted from private to public (while abac is disabled) to retain its abac attributes (if any) diff --git a/.changeset/clever-zebras-melt.md b/.changeset/clever-zebras-melt.md new file mode 100644 index 0000000000000..8d02ede87a233 --- /dev/null +++ b/.changeset/clever-zebras-melt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where thread content would disappear after clicking "Jump to recent messages". diff --git a/.changeset/ddp-client-idempotent-reconnect.md b/.changeset/ddp-client-idempotent-reconnect.md new file mode 100644 index 0000000000000..207644c5211ad --- /dev/null +++ b/.changeset/ddp-client-idempotent-reconnect.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ddp-client": patch +--- + +Make `Connection.connect()` and `Connection.reconnect()` idempotent. Previously they rejected with `Error('Connection in progress')` when called while a connection was already in flight or established. Because the internal retry timer (`ws.onclose` → `setTimeout(() => void this.reconnect(), …)`) fires with no `.catch`, that rejection surfaced as an unhandled rejection at the page level whenever an external caller (e.g. an SDK consumer's bootstrap path) won the race against the timer. While `status === 'connecting'`, both methods now return the in-flight handshake promise so a later `failed` payload still propagates to every caller instead of being masked by a synthesized success; while `status === 'connected'` they resolve with `true`. The timer also no-ops when the connection has already been re-established, and a stale `ws.onclose` from a replaced socket no longer clobbers the new socket's status or schedules a redundant retry. diff --git a/.changeset/ddp-client-reset-retry-count.md b/.changeset/ddp-client-reset-retry-count.md new file mode 100644 index 0000000000000..90730285d3967 --- /dev/null +++ b/.changeset/ddp-client-reset-retry-count.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ddp-client": patch +--- + +Reset `Connection.retryCount` to zero on a successful (re)connection. The counter was only ever incremented (in `ws.onclose`), never zeroed, so the retry budget was monotonically consumed across the connection's lifetime. With the default budget of `1`, any chain of two close events — for example the SDK reconnecting after a server force-logout, then the client running a follow-up `Meteor.logout()` whose server handler closes the WS again — drained the budget; the second close handler bailed at `retryCount >= retryOptions.retryCount` and the SDK stayed permanently disconnected. Method frames already in the dispatcher queue (e.g. a fresh `login` retry from the consumer) stayed queued forever. diff --git a/.changeset/ddp-dispatcher-non-method-frames.md b/.changeset/ddp-dispatcher-non-method-frames.md new file mode 100644 index 0000000000000..c54d34e3b5164 --- /dev/null +++ b/.changeset/ddp-dispatcher-non-method-frames.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ddp-client": patch +--- + +Fix `DDPDispatcher` dropping non-method frames (connect, sub, unsub, ping, pong) when a `wait` block is at the head of the queue. Previously every payload flowed through the same wait-serialization path: a `connect` frame dispatched after a `wait: true` method (e.g. `login`) would be queued in a new non-wait block but never actually sent, wedging the DDP handshake — the socket stayed open, the server never replied `connected`, and any caller awaiting the connection hung. Non-method payloads now bypass the queue and emit immediately; wait-method serialization between methods is unchanged. diff --git a/.changeset/dry-squids-wonder.md b/.changeset/dry-squids-wonder.md new file mode 100644 index 0000000000000..7fa1417da93cc --- /dev/null +++ b/.changeset/dry-squids-wonder.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing edited indicator for the main parent message in the thread panel to ensure visual consistency with the main channel view. diff --git a/.changeset/edit-room-info-multiselect-aria-label.md b/.changeset/edit-room-info-multiselect-aria-label.md new file mode 100644 index 0000000000000..0c5b39192087d --- /dev/null +++ b/.changeset/edit-room-info-multiselect-aria-label.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds an accessible label to the system-messages multi-select in the channel edit panel so screen readers announce its purpose. diff --git a/.changeset/eight-clouds-count.md b/.changeset/eight-clouds-count.md new file mode 100644 index 0000000000000..25ecce7dc886a --- /dev/null +++ b/.changeset/eight-clouds-count.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes intermittent "Channel Not Joined" screen when opening rooms in embedded mode. diff --git a/.changeset/fix-presence-comma-ids.md b/.changeset/fix-presence-comma-ids.md new file mode 100644 index 0000000000000..c85d20785e3a6 --- /dev/null +++ b/.changeset/fix-presence-comma-ids.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/rest-typings': patch +--- + +Fixes the `users.presence` endpoint returning an empty array when called with multiple comma-separated IDs, caused by `ajvQuery` coercing the string into a single-element array after the OpenAPI migration \ No newline at end of file diff --git a/.changeset/flat-islands-nail.md b/.changeset/flat-islands-nail.md new file mode 100644 index 0000000000000..10e5f4be74d88 --- /dev/null +++ b/.changeset/flat-islands-nail.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ui-voip": patch +--- + +Fixes an issue where intentionally cancelling a screen share request would show an error toast by identifying error types and counting consecutive failures. diff --git a/.changeset/good-rules-lie.md b/.changeset/good-rules-lie.md new file mode 100644 index 0000000000000..75542e75aa078 --- /dev/null +++ b/.changeset/good-rules-lie.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Ensures that deactivated users have their login tokens cleaned up in users.deactivateidle diff --git a/.changeset/hot-impalas-vanish.md b/.changeset/hot-impalas-vanish.md new file mode 100644 index 0000000000000..30c32de851fd5 --- /dev/null +++ b/.changeset/hot-impalas-vanish.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Hides the room announcement, topic and description from the Administration > Rooms panel for ABAC managed rooms. In the channel sidebar Edit Channel form those fields stay visible to room members but are disabled, and the API rejects edits to them. diff --git a/.changeset/hot-spoons-heal.md b/.changeset/hot-spoons-heal.md new file mode 100644 index 0000000000000..6bcdabec83e39 --- /dev/null +++ b/.changeset/hot-spoons-heal.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Disables SAML login when it is set to validate signatures without the proper configuration for it diff --git a/.changeset/little-cars-love.md b/.changeset/little-cars-love.md new file mode 100644 index 0000000000000..7b67fa9c679be --- /dev/null +++ b/.changeset/little-cars-love.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/i18n": minor +"@rocket.chat/ui-voip": minor +--- + +Adds "Open in room" button to shared screen card on voice call widget diff --git a/.changeset/lovely-laws-wait.md b/.changeset/lovely-laws-wait.md new file mode 100644 index 0000000000000..306cfc7793926 --- /dev/null +++ b/.changeset/lovely-laws-wait.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Allows users to search for attribute values when assigning them to rooms diff --git a/.changeset/major-coats-smash.md b/.changeset/major-coats-smash.md new file mode 100644 index 0000000000000..387bc777e041f --- /dev/null +++ b/.changeset/major-coats-smash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes test button not playing default sound in Notifications Preferences diff --git a/.changeset/mighty-icons-kiss.md b/.changeset/mighty-icons-kiss.md new file mode 100644 index 0000000000000..5d62298c8e0c7 --- /dev/null +++ b/.changeset/mighty-icons-kiss.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +'@rocket.chat/i18n': minor +--- + +Adds new API endpoints `custom-sounds.create` and `custom-sounds.update` to manage custom sounds with strict file validation for size and specific MIME types to ensure system compatibility. diff --git a/.changeset/neat-trams-juggle.md b/.changeset/neat-trams-juggle.md new file mode 100644 index 0000000000000..56f327f68cc0c --- /dev/null +++ b/.changeset/neat-trams-juggle.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Ensures the Meteor method for translateMessage validates access and types diff --git a/.changeset/ninety-lilies-kick.md b/.changeset/ninety-lilies-kick.md new file mode 100644 index 0000000000000..f507a7fbfe4e0 --- /dev/null +++ b/.changeset/ninety-lilies-kick.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/gazzodown': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes Insert Timestamp relative time preview not updating on input changes and losing the user's locale after the first refresh tick. diff --git a/.changeset/orange-bottles-glow.md b/.changeset/orange-bottles-glow.md new file mode 100644 index 0000000000000..ffc78906db762 --- /dev/null +++ b/.changeset/orange-bottles-glow.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes signed URL generation for S3 and Google Cloud Storage when the expiry setting is below 5 seconds, which previously caused expired or invalid preview URLs. Adds a dedicated URL expiry setting for Google Cloud Storage since it was incorrectly reusing the AWS S3 setting. diff --git a/.changeset/perky-tires-invite.md b/.changeset/perky-tires-invite.md new file mode 100644 index 0000000000000..d3824c95cfca4 --- /dev/null +++ b/.changeset/perky-tires-invite.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Ensures the visitor token is not present in the visitors.info response diff --git a/.changeset/pink-moons-cheer.md b/.changeset/pink-moons-cheer.md new file mode 100644 index 0000000000000..e753031cdb868 --- /dev/null +++ b/.changeset/pink-moons-cheer.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/apps": minor +--- + +Allows apps with the right permission to read room's ABAC attributes. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..43339a932fd18 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,131 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "8.5.0-develop", + "rocketchat-services": "2.0.47", + "@rocket.chat/uikit-playground": "0.7.12", + "@rocket.chat/account-service": "0.4.56", + "@rocket.chat/authorization-service": "0.6.0", + "@rocket.chat/ddp-streamer": "0.3.56", + "@rocket.chat/omnichannel-transcript": "0.4.56", + "@rocket.chat/presence-service": "0.4.56", + "@rocket.chat/queue-worker": "0.4.56", + "@rocket.chat/abac": "0.2.0", + "@rocket.chat/federation-matrix": "0.1.3", + "@rocket.chat/license": "1.1.16", + "@rocket.chat/media-calls": "0.4.0", + "@rocket.chat/network-broker": "0.2.35", + "@rocket.chat/omni-core-ee": "0.0.21", + "@rocket.chat/omnichannel-services": "0.3.53", + "@rocket.chat/pdf-worker": "0.3.35", + "@rocket.chat/presence": "0.2.56", + "@rocket.chat/account-utils": "0.0.3", + "@rocket.chat/agenda": "0.1.1", + "@rocket.chat/api-client": "0.2.56", + "@rocket.chat/apps": "0.6.9", + "@rocket.chat/apps-engine": "1.62.0", + "@rocket.chat/base64": "1.0.14", + "@rocket.chat/cas-validate": "0.0.4", + "@rocket.chat/core-services": "0.14.0", + "@rocket.chat/core-typings": "8.5.0-develop", + "@rocket.chat/cron": "0.1.56", + "@rocket.chat/ddp-client": "1.0.9", + "@rocket.chat/desktop-api": "1.1.1", + "@rocket.chat/eslint-config": "0.8.0", + "@rocket.chat/favicon": "0.0.5", + "@rocket.chat/fuselage-ui-kit": "30.0.0", + "@rocket.chat/gazzodown": "30.0.0", + "@rocket.chat/http-router": "7.9.23", + "@rocket.chat/i18n": "3.0.0", + "@rocket.chat/instance-status": "0.1.56", + "@rocket.chat/jest-presets": "0.0.2", + "@rocket.chat/jwt": "0.2.1", + "@rocket.chat/livechat": "2.1.3", + "@rocket.chat/log-format": "0.0.3", + "@rocket.chat/logger": "1.0.1", + "@rocket.chat/media-signaling": "1.0.0", + "@rocket.chat/message-parser": "0.31.36", + "@rocket.chat/message-types": "0.1.1", + "@rocket.chat/mock-providers": "0.4.16", + "@rocket.chat/model-typings": "2.2.0", + "@rocket.chat/models": "2.2.0", + "@rocket.chat/mongo-adapter": "0.0.3", + "@rocket.chat/poplib": "0.0.3", + "@rocket.chat/omni-core": "0.1.0", + "@rocket.chat/password-policies": "0.1.1", + "@rocket.chat/patch-injection": "0.0.2", + "@rocket.chat/peggy-loader": "0.31.28", + "@rocket.chat/random": "1.2.3", + "@rocket.chat/release-action": "2.2.4", + "@rocket.chat/release-changelog": "0.1.1", + "@rocket.chat/rest-typings": "8.5.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.3", + "@rocket.chat/server-fetch": "0.2.0", + "@rocket.chat/sha256": "1.0.13", + "@rocket.chat/storybook-config": "0.0.3", + "@rocket.chat/tools": "0.3.0", + "@rocket.chat/tracing": "0.0.2", + "@rocket.chat/tsconfig": "0.0.0", + "@rocket.chat/ui-avatar": "26.0.0", + "@rocket.chat/ui-client": "30.0.0", + "@rocket.chat/ui-composer": "1.0.0", + "@rocket.chat/ui-contexts": "30.0.0", + "@rocket.chat/ui-kit": "1.0.0", + "@rocket.chat/ui-video-conf": "30.0.0", + "@rocket.chat/ui-voip": "20.0.0", + "@rocket.chat/web-ui-registration": "30.0.0" + }, + "changesets": [ + "angry-baboons-trade", + "beige-parrots-lead", + "big-corners-tie", + "brave-fans-tie", + "brown-files-work", + "bump-patch-1779457640608", + "bump-patch-1780029980454", + "bump-patch-1780545712119", + "bump-patch-1780685193955", + "bump-patch-1781098424278", + "bump-patch-1781123233262", + "calm-seas-run", + "clever-zebras-melt", + "ddp-client-idempotent-reconnect", + "ddp-client-reset-retry-count", + "ddp-dispatcher-non-method-frames", + "dry-squids-wonder", + "edit-room-info-multiselect-aria-label", + "eight-clouds-count", + "fix-presence-comma-ids", + "flat-islands-nail", + "good-rules-lie", + "hot-impalas-vanish", + "hot-spoons-heal", + "little-cars-love", + "lovely-laws-wait", + "major-coats-smash", + "mighty-icons-kiss", + "neat-trams-juggle", + "ninety-lilies-kick", + "orange-bottles-glow", + "perky-tires-invite", + "pink-moons-cheer", + "public-ravens-like", + "red-maps-wink", + "rich-doodles-grow", + "room-search-index", + "sanitize-image-element-url", + "sdk-account-event-handlers", + "shaggy-kiwis-burn", + "sharp-planets-throw", + "silent-coats-open", + "spicy-phones-breathe", + "stale-needles-care", + "ten-lizards-divide", + "tidy-donuts-check", + "tricky-kiwis-sit", + "use-format-date-fallback", + "use-rc-sdk-transport-setting", + "witty-ravens-end" + ] +} diff --git a/.changeset/public-ravens-like.md b/.changeset/public-ravens-like.md new file mode 100644 index 0000000000000..95604fc9c8fcd --- /dev/null +++ b/.changeset/public-ravens-like.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Adds the capability for fetching a user by their sip extension to the apps diff --git a/.changeset/red-maps-wink.md b/.changeset/red-maps-wink.md new file mode 100644 index 0000000000000..c6f130650b185 --- /dev/null +++ b/.changeset/red-maps-wink.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a new "Drafts" group to the sidebar, providing quick access to all rooms with unfinished messages. +> This feature is available under the `Drafts in sidebar` feature preview and needs to be enabled in settings to be tested. \ No newline at end of file diff --git a/.changeset/rich-doodles-grow.md b/.changeset/rich-doodles-grow.md new file mode 100644 index 0000000000000..3699a28550a66 --- /dev/null +++ b/.changeset/rich-doodles-grow.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/room-search-index.md b/.changeset/room-search-index.md new file mode 100644 index 0000000000000..b7832034ba5ba --- /dev/null +++ b/.changeset/room-search-index.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/models': minor +--- + +Adds the `USE_ROOM_SEARCH_INDEX` environment variable. When set to `true`, the messages collection's text index is created as `{ rid: 1, msg: 'text' }` instead of the default `{ msg: 'text' }`. The compound shape lets per-room `$text` searches use `rid` as a prefix, dramatically reducing the portion of the index scanned on workspaces where global search is disabled. + +The index is reconciled on every startup: if the existing text index already matches the desired shape, nothing happens; otherwise the stale text index is dropped and the desired one is recreated. Unsetting the variable on a later boot reverts to the default shape. diff --git a/.changeset/sanitize-image-element-url.md b/.changeset/sanitize-image-element-url.md new file mode 100644 index 0000000000000..c2a3a18af8027 --- /dev/null +++ b/.changeset/sanitize-image-element-url.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/gazzodown': patch +'@rocket.chat/meteor': patch +--- + +Sanitizes image URLs in rendered messages to block `javascript:`, `data:`, and `vbscript:` schemes — matching the protection already applied to markdown links. Defense-in-depth against XSS via crafted markdown like `![label](javascript:...)`. diff --git a/.changeset/sdk-account-event-handlers.md b/.changeset/sdk-account-event-handlers.md new file mode 100644 index 0000000000000..ed7bb38fa2d46 --- /dev/null +++ b/.changeset/sdk-account-event-handlers.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/ddp-client': minor +--- + +Add lifecycle event handlers to `Account`: `onLogin`, `onLogout`, `onEmailVerificationLink`, and `onPageLoadLogin`. `onLogin` / `onLogout` fire on `uid` transitions (the setter now emits only when the value changes, so a single login or logout produces exactly one callback). `onEmailVerificationLink` and `onPageLoadLogin` are convenience wrappers around new `Emitter` events of the same names — fire them externally with `account.emit('emailVerificationLink', token)` / `account.emit('pageLoadLogin', loginAttempt)`. The bridge from Meteor's accounts-base lives in the consumer (`apps/meteor/client`) so the package stays Meteor-independent. All four handlers return an unsubscribe function. diff --git a/.changeset/shaggy-kiwis-burn.md b/.changeset/shaggy-kiwis-burn.md new file mode 100644 index 0000000000000..6a5834f65d4cf --- /dev/null +++ b/.changeset/shaggy-kiwis-burn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Ensures the autotranslate.translateMessage endpoint checks for room access diff --git a/.changeset/sharp-planets-throw.md b/.changeset/sharp-planets-throw.md new file mode 100644 index 0000000000000..19e01af823bcd --- /dev/null +++ b/.changeset/sharp-planets-throw.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes action buttons added by apps being rendered in the Marketplace Menu rather than the User Menu diff --git a/.changeset/silent-coats-open.md b/.changeset/silent-coats-open.md new file mode 100644 index 0000000000000..e8e88a7966104 --- /dev/null +++ b/.changeset/silent-coats-open.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the Chat Limits locking mechanism to allow bot agents to skip the lock as they aren't limited diff --git a/.changeset/spicy-phones-breathe.md b/.changeset/spicy-phones-breathe.md new file mode 100644 index 0000000000000..71bb6affc744b --- /dev/null +++ b/.changeset/spicy-phones-breathe.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/abac": patch +--- + +Fixes an issue where some actions made by the abac service were not broadcasting to clients, which affected reactivity diff --git a/.changeset/stale-needles-care.md b/.changeset/stale-needles-care.md new file mode 100644 index 0000000000000..5f9e5b4c893bd --- /dev/null +++ b/.changeset/stale-needles-care.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps': minor +--- + +Receives the migration of internal files from @rocket.chat/apps-engine diff --git a/.changeset/ten-lizards-divide.md b/.changeset/ten-lizards-divide.md new file mode 100644 index 0000000000000..e272a03e1fe04 --- /dev/null +++ b/.changeset/ten-lizards-divide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issue that displayed the 'Delete all closed chats' button when user lacks `remove-closed-livechat-rooms` permission diff --git a/.changeset/tidy-donuts-check.md b/.changeset/tidy-donuts-check.md new file mode 100644 index 0000000000000..5aeb2717a85bc --- /dev/null +++ b/.changeset/tidy-donuts-check.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds `freeSwitchExtension` as a query parameter for `api/v1/users.info` diff --git a/.changeset/tricky-kiwis-sit.md b/.changeset/tricky-kiwis-sit.md new file mode 100644 index 0000000000000..eac1db7222c36 --- /dev/null +++ b/.changeset/tricky-kiwis-sit.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-kit': minor +'@rocket.chat/meteor': minor +--- + +Adds support to room information on ViewSubmit and ViewClose events for ContextualBar surface diff --git a/.changeset/use-format-date-fallback.md b/.changeset/use-format-date-fallback.md new file mode 100644 index 0000000000000..6cd6197f2e739 --- /dev/null +++ b/.changeset/use-format-date-fallback.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a `date-fns` crash on routes that mount before the public settings stream finishes loading. `useFormatDate` was passing `String(undefined)` (the literal `"undefined"`) to `formatDate` while `Message_DateFormat` was momentarily unloaded — `date-fns` rejects that token because it contains an unescaped `n`. The hook now uses `'LL'` as the default token via `useSetting`'s second argument, so the formatter always receives a valid format string. diff --git a/.changeset/use-rc-sdk-transport-setting.md b/.changeset/use-rc-sdk-transport-setting.md new file mode 100644 index 0000000000000..7e43aa7437fda --- /dev/null +++ b/.changeset/use-rc-sdk-transport-setting.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Adds a new admin setting `Use_RC_SDK` (General → Use Rocket.Chat SDK) that opts the workspace into the experimental SDK-over-DDP transport. When enabled, the client routes Meteor DDP traffic through `@rocket.chat/ddp-client` over a single WebSocket instead of the legacy Meteor stream. The flag is dormant by default; the server surfaces the value via a `` tag, and the client also honors a per-tab `?sdk_transport=on|off` URL parameter and a `rc-config-sdk_transport` localStorage key (URL > localStorage > meta tag). diff --git a/.changeset/witty-ravens-end.md b/.changeset/witty-ravens-end.md new file mode 100644 index 0000000000000..975623f6a1d8e --- /dev/null +++ b/.changeset/witty-ravens-end.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-calls': minor +--- + +Fixes a server error when Drachtio connection is lost while a SIP call is active diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 77ebf4aa3ad03..2d7b1fa736a18 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -99,7 +99,7 @@ runs: find /tmp/build/bundle/programs/server/npm/node_modules/@esbuild -type d -name 'linux-*' -not -name "*-${swc_arch}" -exec rm -rf {} + - find /tmp/build/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/node_modules/@esbuild -type d -name 'linux-*' -not -name "*-${swc_arch}" -exec rm -rf {} + + find /tmp/build/bundle/programs/server/npm/node_modules/@rocket.chat/apps/node_modules/@esbuild -type d -name 'linux-*' -not -name "*-${swc_arch}" -exec rm -rf {} + fi if [[ "$INPUT_PUBLISH_IMAGE" == 'true' ]]; then diff --git a/.github/actions/update-version-durability/action.yml b/.github/actions/update-version-durability/action.yml index 07375ac383b87..85e4ec2f8fd64 100644 --- a/.github/actions/update-version-durability/action.yml +++ b/.github/actions/update-version-durability/action.yml @@ -14,6 +14,9 @@ inputs: D360_ARTICLE_ID: required: true description: Document360 Article ID + D360_USER_ID: + required: true + description: Document360 User ID used to fork and publish the article PUBLISH: required: true description: Publish Draft diff --git a/.github/actions/update-version-durability/index.js b/.github/actions/update-version-durability/index.js index d5fbeffb5de49..062f13e834d7d 100644 --- a/.github/actions/update-version-durability/index.js +++ b/.github/actions/update-version-durability/index.js @@ -11,6 +11,7 @@ import { Octokit } from '@octokit/rest'; const D360_TOKEN = core.getInput('D360_TOKEN'); const D360_ARTICLE_ID = core.getInput('D360_ARTICLE_ID'); +const D360_USER_ID = core.getInput('D360_USER_ID'); const PUBLISH = core.getInput('PUBLISH') === 'true'; const LTS_VERSIONS = (core.getInput('LTS_VERSIONS') || '7.10').split(',').map((v) => v.trim()); @@ -193,11 +194,15 @@ async function generateTable({ owner, repo } = {}) { const forkResponse = await requestDocument360('put', `Articles/${D360_ARTICLE_ID}/fork`, { lang_code: "en", - user_id: "2511fd00-9558-4826-8d8c-4cc0c110f89c", + user_id: D360_USER_ID, version_number: response.data.data.version_number, }); console.log(forkResponse.data); + + if (!forkResponse.data.success) { + throw new Error(`Failed to fork article: ${JSON.stringify(forkResponse.data.errors)}`); + } } console.log('Updating article'); @@ -211,7 +216,7 @@ async function generateTable({ owner, repo } = {}) { console.log('publishing article', updateResponse.data.data.version_number); const forkResponse = await requestDocument360('post', `Articles/${D360_ARTICLE_ID}/en/publish`, { - user_id: "2511fd00-9558-4826-8d8c-4cc0c110f89c", + user_id: D360_USER_ID, version_number: updateResponse.data.data.version_number, publish_message: 'Update support versions table via GitHub Action', }); diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index 5646f81db24e4..cd65c8d5e37b2 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@octokit/rest": "^21.0.0", - "@xmldom/xmldom": "^0.8.10", + "@xmldom/xmldom": "^0.8.13", "axios": "^1.7.2", "beauty-html": "^1.3.1", "colors": "^1.4.0", @@ -194,9 +194,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -209,9 +209,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -272,9 +272,10 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml deleted file mode 100644 index 587d4c2b3c36f..0000000000000 --- a/.github/workflows/actionlint.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Lint GitHub Actions workflows -on: [push, pull_request] - -jobs: - actionlint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Check workflow files - run: | - echo "::add-matcher::.github/actionlint-matcher.json" - ACTIONLINT_VERSION=1.7.8 - curl -fsSLO "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" - curl -fsSLO "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" - grep " actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz$" "actionlint_${ACTIONLINT_VERSION}_checksums.txt" | sha256sum -c - - tar -xzf "actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" actionlint - ./actionlint -color - shell: bash diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 916c0ccb6954e..4416a9f8ada5a 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -41,7 +41,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - uses: ./.github/actions/restore-packages diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index cd777648842a4..dc6b044dbd19b 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -30,7 +30,7 @@ jobs: mv ${{ github.ref_name }} .preview - name: Deploy - uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .preview diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 8917f2e90325c..f90122800df9c 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -125,7 +125,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - uses: ./.github/actions/restore-packages diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 91a39aecc3c5b..144b5895ffc18 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -37,7 +37,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - uses: ./.github/actions/restore-packages @@ -48,7 +48,7 @@ jobs: env: STORYBOOK_DISABLE_TELEMETRY: 1 - - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: flags: unit verbose: true diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index f7c902bcb553d..13ea1f31d6490 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -41,14 +41,14 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - uses: ./.github/actions/restore-packages - name: Unit Test run: yarn testunit --concurrency=1 - - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: flags: unit verbose: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56a124952b9c8..cc8e1c7f9018c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: source-hash: ${{ steps.source.outputs.hash }} packages-build-cache-key: ${{ steps.ci-cache-keys.outputs.packages-build-cache-key }} meteor-rc-cache-key: ${{ steps.ci-cache-keys.outputs.meteor-rc-cache-key }} + github-actions-changed: ${{ steps.diff.outputs.actions-changed }} # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail # to avoid this, we are using a dummy license, expiring at 2026-07-01 enterprise-license: Uo7Jcr6WW0XYA8ydHd+Sk6pZ9/0V6dIASnyTwvUrNym/zJg2Ma3eYNKkC8osXLCc72y1ahohnWY7/+7IYkvono3GYXQR+IGvYbbrVgNR6OjMahd9P/odHZL1GFTm2qHrEL5Hh/XEOG+YluFeRdWPzCizQlp4zGGOi0+PkQo096TR9NVCLrsErVl2MW1WM6ZM1W5EUJG9pKly4BQnaOTUAlor1im6i8qPTDCKrISZfLiZEWuQKaPW/GE3mRKjQNjDh0CabX1N2S880pRRGoozBYAnp2NmFfrQW0+5ihKisBTIeMbMZ7K5NE5PkYU1nhQDcc+rpDHtwG9Ceg5X0J+oea3UfrPTmDON2aSI0iO22kvL6G7QI3fyrEIvJrMbxcNKxAFeQYgnjisw/b06+chWSG4jG686Fx58XrVS87dFhWL9WoGltsk1dJCntUQvI1sX6zOfpvyg1iWRnHfYDOrwoWlX57XMm29fWineEoqnOOTOVnA/uP+DKEhercQ9Xuo7Cr6zJxpQpwd03e7ODVjiEbTDqlkZE687rmxRCD4Wmu8L86WIl2xSEIajKLX301Ww5mz/FdLqk+Mg32lkW66W3azQKvJ1440NBrYxhpJ+dl9vSFMb3s1+xnz1cYUbjUcq9mARvORcgy5mLwKulmqT6Sq0Uvbv10YCO0TW0beXYW8= @@ -133,6 +134,16 @@ jobs: echo "DOCKER_TAG: ${DOCKER_TAG}" echo "gh-docker-tag=${DOCKER_TAG}" >> "$GITHUB_OUTPUT" + - id: diff + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_PR_NUM: ${{ github.event.number }} + run: | + if gh pr diff --name-only --repo "${GITHUB_REPOSITORY}" "${GH_PR_NUM}" | grep -q -e .github/workflows -e .github/actions; then + echo "Workflow changes detected" + echo "actions-changed=true" >> "$GITHUB_OUTPUT" + fi + notify-draft-services: name: 🚀 Notify external services - draft runs-on: ubuntu-24.04-arm @@ -235,7 +246,7 @@ jobs: restore-keys: | vite-local-cache-${{ runner.arch }}-${{ runner.os }}- - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 if: steps.packages-cache-build.outputs.cache-hit != 'true' - name: Build Rocket.Chat Packages @@ -479,6 +490,32 @@ jobs: baseline-tag: develop size-thresholds: '{"rocketchat":{"mb":11}}' + actionlint: + name: 🔎 Code Check - Action Lint + needs: [release-versions] + if: needs.release-versions.outputs.github-actions-changed + runs-on: ubuntu-24.04-arm + env: + ACTIONLINT_VERSION: 1.7.12 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Get actionlint + shell: bash + run: | + TAR_NAME="actionlint_${ACTIONLINT_VERSION}_$(echo "${RUNNER_OS}_${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]').tar.gz" + echo "Downloading ${TAR_NAME}..." + curl -fsSLO "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${TAR_NAME}" + curl -fsSLo checksums.txt "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" + grep " ${TAR_NAME}$" "checksums.txt" | sha256sum -c - + tar -xzf "${TAR_NAME}" actionlint + + - name: Run actionlint + shell: bash + run: | + ./actionlint -color + + checks: needs: [release-versions, packages-build] @@ -658,7 +695,7 @@ jobs: cache-modules: true install: true - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Restore turbo build uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -772,7 +809,7 @@ jobs: include-hidden-files: true - name: Report API coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: files: /tmp/coverage_report/api/lcov.info working-directory: . @@ -781,7 +818,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Report UI coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: files: /tmp/coverage_report/ui/lcov.info working-directory: . @@ -1063,3 +1100,4 @@ jobs: secrets: CI_PAT: ${{ secrets.CI_PAT }} D360_TOKEN: ${{ secrets.D360_TOKEN }} + D360_USER_ID: ${{ secrets.D360_USER_ID }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0671ab9084bd1..b6ccbed4870e1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -36,7 +36,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -50,4 +50,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 1c19f6bf6b018..577a1453eb1c3 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -38,7 +38,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Build packages run: yarn build diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index af1c0902a99c1..89e29fc3505a5 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -25,7 +25,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Build packages run: yarn build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 407891b1897b2..699e234554101 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Build packages run: yarn build diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 8647d15591f52..376120c91d383 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -19,7 +19,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@00a0515f175df9fd2e15c4560144ad5fdbebb0c7 # v2.3.13 + - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 - name: Build packages run: yarn build diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 5bd2b83c02079..11fa82711ae1e 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -12,6 +12,8 @@ on: required: true D360_TOKEN: required: true + D360_USER_ID: + required: true jobs: update-versions: @@ -23,7 +25,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.22.2 + node-version: 22.22.3 - name: Install dependencies run: | @@ -37,4 +39,5 @@ jobs: GH_TOKEN: ${{ secrets.CI_PAT }} D360_TOKEN: ${{ secrets.D360_TOKEN }} D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb + D360_USER_ID: ${{ secrets.D360_USER_ID }} PUBLISH: true diff --git a/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch b/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch index e8fdaef6e084e..833b21fc29fff 100644 --- a/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch +++ b/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch @@ -1,7 +1,16 @@ diff --git a/lib/@types/index.d.ts b/lib/@types/index.d.ts -index f71a82f458c1432202be8d4585fc70ba94bee4a4..b874ee9e1fa8a051f06d4824fc12161acfadb78a 100644 +index f71a82f458c1432202be8d4585fc70ba94bee4a4..2ad273d1efcaaa54b0a1326ff1c15b6cfc199be3 100644 --- a/lib/@types/index.d.ts +++ b/lib/@types/index.d.ts +@@ -101,7 +101,7 @@ declare namespace Srf { + local: { uri: string; sdp: string; }; + remote: { uri: string; sdp: string; }; + req: SrfRequest; +- destroy(opts?: { headers: Record; }, callback?: (err: any, msg: SrfRequest) => void): void; ++ destroy(opts?: { headers: Record; }, callback?: (err: any, msg: SrfRequest) => void): Promise; + modify(sdp: string, opts?: { noAck: boolean }): Promise; + modify(opts: { noAck: boolean }): Promise; + modify(sdp: string, opts?: { noAck: boolean }, callback?: (err: any, msg: SrfResponse) => void): void; @@ -119,12 +119,12 @@ declare namespace Srf { on(messageType: "modify", callback: (req: SrfRequest, res: SrfResponse) => void): void; once(messageType: string, callback: (msg: SrfResponse) => void): void; diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile deleted file mode 100644 index f9f899be75d18..0000000000000 --- a/apps/meteor/.docker-mongo/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -FROM node:22.22.2-bullseye-slim - -LABEL maintainer="buildmaster@rocket.chat" - -# Install MongoDB and dependencies -ENV MONGO_MAJOR=5.0 \ - MONGO_VERSION=5.0.5 - -RUN set -x \ - && apt-get update \ - && apt-get install -y wget gnupg dirmngr pwgen \ - && wget -qO - "https://www.mongodb.org/static/pgp/server-$MONGO_MAJOR.asc" | apt-key add - \ - && echo "deb http://repo.mongodb.org/apt/debian buster/mongodb-org/$MONGO_MAJOR main" | tee "/etc/apt/sources.list.d/mongodb-org-$MONGO_MAJOR.list" \ - && apt-get update \ - && apt-get install -y \ - mongodb-org=$MONGO_VERSION \ - mongodb-org-server=$MONGO_VERSION \ - mongodb-org-shell=$MONGO_VERSION \ - mongodb-org-mongos=$MONGO_VERSION \ - mongodb-org-tools=$MONGO_VERSION \ - fontconfig \ - && apt-get clean my room \ - && groupadd -g 65533 -r rocketchat \ - && useradd -u 65533 -r -g rocketchat rocketchat \ - && mkdir -p /app/uploads \ - && chown rocketchat:rocketchat /app/uploads - -# --chown requires Docker 17.12 and works only on Linux -ADD --chown=rocketchat:rocketchat . /app -ADD --chown=rocketchat:rocketchat entrypoint.sh /app/bundle/ - -RUN aptMark="$(apt-mark showmanual)" \ - && apt-get install -y --no-install-recommends g++ make python ca-certificates \ - && cd /app/bundle/programs/server \ - && npm install \ - && apt-mark auto '.*' > /dev/null \ - && apt-mark manual $aptMark > /dev/null \ - && find /usr/local -type f -executable -exec ldd '{}' ';' \ - | awk '/=>/ { print $(NF-1) }' \ - | sort -u \ - | xargs -r dpkg-query --search \ - | cut -d: -f1 \ - | sort -u \ - | xargs -r apt-mark manual \ - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && npm cache clear --force - -VOLUME /app/uploads - -WORKDIR /app/bundle - -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker-preview \ - NODE_ENV=production \ - MONGO_URL=mongodb://localhost:27017/rocketchat \ - MONGO_OPLOG_URL=mongodb://localhost:27017/local \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - -EXPOSE 3000 - -RUN chmod +x /app/bundle/entrypoint.sh - -ENTRYPOINT /app/bundle/entrypoint.sh diff --git a/apps/meteor/.docker-mongo/entrypoint.sh b/apps/meteor/.docker-mongo/entrypoint.sh deleted file mode 100644 index 0568da462bf32..0000000000000 --- a/apps/meteor/.docker-mongo/entrypoint.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash - -echo """ - -  -  -  -    -       -         -            -            -            -          -          -           -           -           -        -              -            -         -         -     -        -   - - - - -""" - -echo """ -██████╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗ ██████╗██╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗██╗ ██╗██╗███████╗██╗ ██╗ -██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔════╝██║ ██║██╔══██╗╚══██╔══╝ ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔════╝██║ ██║ -██████╔╝██║ ██║██║ █████╔╝ █████╗ ██║ ██║ ███████║███████║ ██║ ██████╔╝██████╔╝█████╗ ██║ ██║██║█████╗ ██║ █╗ ██║ -██╔══██╗██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██║ ██╔══██║██╔══██║ ██║ ██╔═══╝ ██╔══██╗██╔══╝ ╚██╗ ██╔╝██║██╔══╝ ██║███╗██║ -██║ ██║╚██████╔╝╚██████╗██║ ██╗███████╗ ██║██╗╚██████╗██║ ██║██║ ██║ ██║ ██║ ██║ ██║███████╗ ╚████╔╝ ██║███████╗╚███╔███╔╝ -╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚═╝╚══════╝ ╚══╝╚══╝ -""" - -mongod --fork --replSet rs0 --config /etc/mongod.conf - -until mongo --eval "db" &> /dev/null; do - echo "MongoDB still not ready, sleeping" - sleep 1 -done - -sleep 2 - -# initiate mongo replica set -for i in `seq 1 30`; do - mongo rocketchat --eval " - rs.initiate({ - _id: 'rs0', - members: [ { _id: 0, host: 'localhost:27017' } ]})" && - s=$? && break || s=$?; - echo "Tried $i times. Waiting 5 secs..."; - sleep 5; -done; - -sleep 2 - -# try multiple times until replica set is ready -for i in `seq 1 30`; do - node main.js && - s=$? && break || s=$?; - echo "Tried $i times. Waiting 5 secs..."; - sleep 5; -done; diff --git a/apps/meteor/.docker-mongo/licenses/LICENSE b/apps/meteor/.docker-mongo/licenses/LICENSE deleted file mode 100644 index 96cd938114191..0000000000000 --- a/apps/meteor/.docker-mongo/licenses/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015-2022 Rocket.Chat Technologies Corp. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index dcfed47e1c5f7..2bee7b45f36e3 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM node:22.22.2-alpine3.22 AS builder +FROM node:22.22.3-alpine3.23 AS builder ENV LANG=C.UTF-8 @@ -20,7 +20,7 @@ RUN cd /app/bundle/programs/server \ && find /app/bundle/programs/server/npm/node_modules -type f -name '*.map' -delete \ && find /app/bundle/programs/web.browser -type f -name '*.map' -delete -FROM node:22.22.2-alpine3.22 +FROM node:22.22.3-alpine3.23 LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.debian b/apps/meteor/.docker/Dockerfile.debian index 8938efd52db18..4279fd4a9196e 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.22.2-bullseye-slim +FROM node:22.22.3-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 5b78dccdc9eb1..27218ad49a49f 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -7,12 +7,12 @@ rocketchat:mongo-config rocketchat:livechat rocketchat:version -accounts-base@3.2.0 +accounts-base@3.2.1 accounts-facebook@1.3.4 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 -accounts-password@3.2.2 +accounts-password@3.2.3 accounts-twitter@1.5.2 google-oauth@1.4.5 @@ -26,9 +26,9 @@ email@3.1.2 meteor-base@1.5.2 ddp-common@1.4.4 -webapp@2.1.0 +webapp@2.1.2 -mongo@2.2.0 +mongo@2.3.0 reload@1.3.2 service-configuration@1.3.5 @@ -50,14 +50,14 @@ tracker@1.3.4 reactive-dict@1.3.2 reactive-var@1.0.13 -standard-minifier-css@1.10.0 +standard-minifier-css@1.10.1 dynamic-import@0.7.4 -ecmascript@0.17.0 -typescript@5.9.3 +ecmascript@0.18.0 +typescript@5.10.0 autoupdate@2.0.1 # photoswipe zodern:types -standard-minifier-js +standard-minifier-js@3.2.0 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 703a72252657a..49a715aec2326 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.4 +METEOR@3.4.1 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 7ea703c812dd1..17ce8c19c40d7 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,31 +1,31 @@ -accounts-base@3.2.0 +accounts-base@3.2.1 accounts-facebook@1.3.4 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 -accounts-password@3.2.2 +accounts-password@3.2.3 accounts-twitter@1.5.2 allow-deny@2.1.0 autoupdate@2.0.1 -babel-compiler@7.13.0 +babel-compiler@7.14.0 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 boilerplate-generator@2.1.0 -callback-hook@1.6.1 +callback-hook@1.7.0 check@1.5.0 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.1.1 +ddp-client@3.2.0 ddp-common@1.4.4 ddp-rate-limiter@1.2.2 -ddp-server@3.1.2 +ddp-server@3.2.0 diff-sequence@1.1.3 dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 -ecmascript@0.17.0 +ecmascript@0.18.0 ecmascript-runtime@0.8.3 -ecmascript-runtime-client@0.12.3 +ecmascript-runtime-client@0.13.0 ecmascript-runtime-server@0.11.1 ejson@1.1.5 email@3.1.2 @@ -41,17 +41,17 @@ id-map@1.2.0 inter-process-messaging@0.1.2 localstorage@1.2.1 logging@1.3.6 -meteor@2.2.0 +meteor@2.3.0 meteor-base@1.5.2 meteor-developer-oauth@1.3.3 meteorhacks:inject-initial@1.0.5 minifier-css@2.0.1 -minifier-js@3.1.0 -minimongo@2.0.5 +minifier-js@3.2.0 +minimongo@2.1.0 modern-browsers@0.2.3 modules@0.20.3 modules-runtime@0.13.2 -mongo@2.2.0 +mongo@2.3.0 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 @@ -78,13 +78,13 @@ session@1.2.2 sha@1.0.10 shell-server@0.7.0 socket-stream-client@0.6.1 -standard-minifier-css@1.10.0 +standard-minifier-css@1.10.1 +standard-minifier-js@3.2.0 tracker@1.3.4 twitter-oauth@1.3.4 -typescript@5.9.3 +typescript@5.10.0 underscore@1.6.4 url@1.3.5 -webapp@2.1.0 +webapp@2.1.2 webapp-hashing@1.1.2 -standard-minifier-js@3.2.0 zodern:types@1.0.13 diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js index ca2657fa8a9a2..94abb12f7beea 100644 --- a/apps/meteor/.mocharc.api.js +++ b/apps/meteor/.mocharc.api.js @@ -10,5 +10,6 @@ module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({ bail: false, retries: 0, file: 'tests/end-to-end/teardown.ts', + reporter: 'tests/end-to-end/reporter.ts', spec: ['tests/end-to-end/api/*.ts', 'tests/end-to-end/api/helpers/**/*', 'tests/end-to-end/api/methods/**/*', 'tests/end-to-end/apps/*'], }); diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index c31b5c6cc79dc..a9eb8493aa394 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,11 +1,208 @@ # @rocket.chat/meteor -## 8.4.3 +## 8.5.0-rc.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.6 + - @rocket.chat/rest-typings@8.5.0-rc.6 +
+ +## 8.5.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.5 + - @rocket.chat/rest-typings@8.5.0-rc.5 +
+ +## 8.5.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.4 + - @rocket.chat/rest-typings@8.5.0-rc.4 +
+ +## 8.5.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. +- ([#40635](https://github.com/RocketChat/Rocket.Chat/pull/40635) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes the Chat Limits locking mechanism to allow bot agents to skip the lock as they aren't limited + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.3 + - @rocket.chat/rest-typings@8.5.0-rc.3 +
+ +## 8.5.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.2 + - @rocket.chat/rest-typings@8.5.0-rc.2 +
+ +## 8.5.0-rc.1 ### Patch Changes - Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.5.0-rc.1 + - @rocket.chat/rest-typings@8.5.0-rc.1 +
+ +## 8.5.0-rc.0 + +### Minor Changes + +- ([#40343](https://github.com/RocketChat/Rocket.Chat/pull/40343)) Swap usage of internal @rocket.chat/apps-engine internal APIs to @rocket.chat/apps package + +- ([#40408](https://github.com/RocketChat/Rocket.Chat/pull/40408)) Adds 4 new permissions (assigned to admins by default) to control the visibility of each tab inside the ABAC Administration panel + +- ([#39760](https://github.com/RocketChat/Rocket.Chat/pull/39760)) ## Phishing-Resistant Multi-Factor Authentication + + Introduces a more secure and reliable server-side OAuth authentication flow. + + ### What’s New + + - **Improved OAuth login security** + OAuth authentication now happens fully on the server, reducing the risk of token theft, phishing attacks, and client-side credential interception. + - **Built-in CSRF, state validation, and PKCE protection** + OAuth logins now include stronger protection against CSRF attacks, request tampering, and authorization code interception through secure state validation and PKCE support. + - **Improved two-step verification with OAuth logins** + Users with email or TOTP two-factor authentication enabled will now be asked to complete 2FA even when signing in with providers like Google, GitHub, GitLab, and others. + - **Improved mobile & desktop app login** + Mobile and desktop apps now support a smoother and more secure deep-link OAuth login flow. + +- ([#40341](https://github.com/RocketChat/Rocket.Chat/pull/40341)) Hides the room announcement, topic and description from the Administration > Rooms panel for ABAC managed rooms. In the channel sidebar Edit Channel form those fields stay visible to room members but are disabled, and the API rejects edits to them. + +- ([#39617](https://github.com/RocketChat/Rocket.Chat/pull/39617)) Adds new API endpoints `custom-sounds.create` and `custom-sounds.update` to manage custom sounds with strict file validation for size and specific MIME types to ensure system compatibility. + +- ([#40463](https://github.com/RocketChat/Rocket.Chat/pull/40463)) Allows apps with the right permission to read room's ABAC attributes. + +- ([#40604](https://github.com/RocketChat/Rocket.Chat/pull/40604)) Adds the capability for fetching a user by their sip extension to the apps + +- ([#38225](https://github.com/RocketChat/Rocket.Chat/pull/38225)) Adds a new "Drafts" group to the sidebar, providing quick access to all rooms with unfinished messages. + > This feature is available under the `Drafts in sidebar` feature preview and needs to be enabled in settings to be tested. +- ([#40397](https://github.com/RocketChat/Rocket.Chat/pull/40397)) Adds the `USE_ROOM_SEARCH_INDEX` environment variable. When set to `true`, the messages collection's text index is created as `{ rid: 1, msg: 'text' }` instead of the default `{ msg: 'text' }`. The compound shape lets per-room `$text` searches use `rid` as a prefix, dramatically reducing the portion of the index scanned on workspaces where global search is disabled. + + The index is reconciled on every startup: if the existing text index already matches the desired shape, nothing happens; otherwise the stale text index is dropped and the desired one is recreated. Unsetting the variable on a later boot reverts to the default shape. + +- ([#40612](https://github.com/RocketChat/Rocket.Chat/pull/40612)) Adds `freeSwitchExtension` as a query parameter for `api/v1/users.info` + +- ([#39858](https://github.com/RocketChat/Rocket.Chat/pull/39858)) Adds support to room information on ViewSubmit and ViewClose events for ContextualBar surface + +- ([#40430](https://github.com/RocketChat/Rocket.Chat/pull/40430)) Adds a new admin setting `Use_RC_SDK` (General → Use Rocket.Chat SDK) that opts the workspace into the experimental SDK-over-DDP transport. When enabled, the client routes Meteor DDP traffic through `@rocket.chat/ddp-client` over a single WebSocket instead of the legacy Meteor stream. The flag is dormant by default; the server surfaces the value via a `` tag, and the client also honors a per-tab `?sdk_transport=on|off` URL parameter and a `rc-config-sdk_transport` localStorage key (URL > localStorage > meta tag). + +### Patch Changes + +- ([#39858](https://github.com/RocketChat/Rocket.Chat/pull/39858)) Fixes an issue that prevented BlockAction interactions from having room information when triggered in a ContextualBar surface + +- ([#40524](https://github.com/RocketChat/Rocket.Chat/pull/40524)) Ensures OAuth tokens are cleaned up after user deactivation + +- ([#40537](https://github.com/RocketChat/Rocket.Chat/pull/40537)) Fixes an issue that allowed a room converted from private to public (while abac is disabled) to retain its abac attributes (if any) + +- ([#39859](https://github.com/RocketChat/Rocket.Chat/pull/39859)) Fixes an issue where thread content would disappear after clicking "Jump to recent messages". + +- ([#40063](https://github.com/RocketChat/Rocket.Chat/pull/40063)) Fixes the missing edited indicator for the main parent message in the thread panel to ensure visual consistency with the main channel view. + +- ([#40357](https://github.com/RocketChat/Rocket.Chat/pull/40357)) Adds an accessible label to the system-messages multi-select in the channel edit panel so screen readers announce its purpose. + +- ([#40100](https://github.com/RocketChat/Rocket.Chat/pull/40100)) Fixes intermittent "Channel Not Joined" screen when opening rooms in embedded mode. + +- ([#40513](https://github.com/RocketChat/Rocket.Chat/pull/40513)) Fixes the `users.presence` endpoint returning an empty array when called with multiple comma-separated IDs, caused by `ajvQuery` coercing the string into a single-element array after the OpenAPI migration + +- ([#40496](https://github.com/RocketChat/Rocket.Chat/pull/40496)) Ensures that deactivated users have their login tokens cleaned up in users.deactivateidle + +- ([#40405](https://github.com/RocketChat/Rocket.Chat/pull/40405)) Disables SAML login when it is set to validate signatures without the proper configuration for it + +- ([#40423](https://github.com/RocketChat/Rocket.Chat/pull/40423)) Allows users to search for attribute values when assigning them to rooms + +- ([#40335](https://github.com/RocketChat/Rocket.Chat/pull/40335)) Fixes test button not playing default sound in Notifications Preferences + +- ([#40528](https://github.com/RocketChat/Rocket.Chat/pull/40528)) Ensures the Meteor method for translateMessage validates access and types + +- ([#40420](https://github.com/RocketChat/Rocket.Chat/pull/40420)) Fixes Insert Timestamp relative time preview not updating on input changes and losing the user's locale after the first refresh tick. + +- ([#40456](https://github.com/RocketChat/Rocket.Chat/pull/40456)) Fixes signed URL generation for S3 and Google Cloud Storage when the expiry setting is below 5 seconds, which previously caused expired or invalid preview URLs. Adds a dedicated URL expiry setting for Google Cloud Storage since it was incorrectly reusing the AWS S3 setting. + +- ([#40501](https://github.com/RocketChat/Rocket.Chat/pull/40501)) Ensures the visitor token is not present in the visitors.info response + +- ([#40405](https://github.com/RocketChat/Rocket.Chat/pull/40405)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#40613](https://github.com/RocketChat/Rocket.Chat/pull/40613)) Sanitizes image URLs in rendered messages to block `javascript:`, `data:`, and `vbscript:` schemes — matching the protection already applied to markdown links. Defense-in-depth against XSS via crafted markdown like `![label](javascript:...)`. + +- ([#40508](https://github.com/RocketChat/Rocket.Chat/pull/40508)) Ensures the autotranslate.translateMessage endpoint checks for room access + +- ([#40448](https://github.com/RocketChat/Rocket.Chat/pull/40448)) Fixes action buttons added by apps being rendered in the Marketplace Menu rather than the User Menu + +- ([#40499](https://github.com/RocketChat/Rocket.Chat/pull/40499)) Fixes an issue where some actions made by the abac service were not broadcasting to clients, which affected reactivity + +- ([#40492](https://github.com/RocketChat/Rocket.Chat/pull/40492)) Fixes issue that displayed the 'Delete all closed chats' button when user lacks `remove-closed-livechat-rooms` permission + +- ([#40393](https://github.com/RocketChat/Rocket.Chat/pull/40393)) Fixes a `date-fns` crash on routes that mount before the public settings stream finishes loading. `useFormatDate` was passing `String(undefined)` (the literal `"undefined"`) to `formatDate` while `Message_DateFormat` was momentarily unloaded — `date-fns` rejects that token because it contains an unescaped `n`. The hook now uses `'LL'` as the default token via `useSetting`'s second argument, so the formatter always receives a valid format string. + +-
Updated dependencies [90f15e32ae843ed146ccf711ee3201408d1e8731, f7d47dd3517ec14ca2ec5c3c95fcdf9e1e2fb8b0, cdb264fec803e234a6ad2000018b31d4b2074e99, 2a927fa1362c9d4bb04bb8e26f23a6e3753d9cea, bede0e2528bb053cb913e93ccf30d78a1c84bc76, bede0e2528bb053cb913e93ccf30d78a1c84bc76, bede0e2528bb053cb913e93ccf30d78a1c84bc76, 4c3984593017d59edd631bf8ae4f35f9d3c3db36, 7f2bdf1809804de7a95c54c3892da30f058ee13d, ae9f740d6af20557eac61b4af902c868b4132b49, b6b04aadfcc8558f888b334e37c46a77e5816237, ad7d42400ea36f1eb0aaf7cc3361c77fdabf9ebc, 4704bf81ca370f120af32185a7c55407a26f8514, d427b808c1f79d9d1baa05bb5b5ef805b6ef5f6d, ebc9babf55dd26613027c28dcacf77909116b342, f392d5cc8d956c199f557dcd6beb52094232499d, 2198d9ea565b06f92e3dec29891890086f62f9df, fac64728505b312d5da786e92d3134450ce4a7c1, 12897e25d0dc25b7373f5264d38f38a5a7444257, e45585b70a3a7b75434c88e4b2ea9af0a0764a76, 0b7a76367d650793c271160e01798ebbb5fe0d26, 51833064591b91140d17e403389e1abbc5d9ef7a, 2d32e52073dd1a68bd12a093b3a673ae297cb4ee, 2a927fa1362c9d4bb04bb8e26f23a6e3753d9cea, b1c2668b74bfb49ebaefe2f581b2f8be5d4d1dd6, 90f15e32ae843ed146ccf711ee3201408d1e8731, 22c8d3283f0ea3004fe94c51f8bb32dfb40a0f4f]: + + - @rocket.chat/ui-kit@1.1.0-rc.0 + - @rocket.chat/model-typings@2.3.0-rc.0 + - @rocket.chat/models@2.3.0-rc.0 + - @rocket.chat/i18n@3.1.0-rc.0 + - @rocket.chat/apps-engine@1.63.0-rc.0 + - @rocket.chat/ddp-client@1.1.0-rc.0 + - @rocket.chat/rest-typings@8.5.0-rc.0 + - @rocket.chat/ui-voip@21.0.0-rc.0 + - @rocket.chat/web-ui-registration@31.0.0-rc.0 + - @rocket.chat/core-typings@8.5.0-rc.0 + - @rocket.chat/gazzodown@31.0.0-rc.0 + - @rocket.chat/apps@0.7.0-rc.0 + - @rocket.chat/ui-client@31.0.0-rc.0 + - @rocket.chat/abac@0.2.1-rc.0 + - @rocket.chat/media-calls@0.5.0-rc.0 + - @rocket.chat/core-services@0.14.1-rc.0 + - @rocket.chat/fuselage-ui-kit@31.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.54-rc.0 + - @rocket.chat/federation-matrix@0.1.4-rc.0 + - @rocket.chat/omni-core-ee@0.0.22-rc.0 + - @rocket.chat/presence@0.2.57-rc.0 + - @rocket.chat/cron@0.1.57-rc.0 + - @rocket.chat/instance-status@0.1.57-rc.0 + - @rocket.chat/omni-core@0.1.1-rc.0 + - @rocket.chat/server-fetch@0.2.1-rc.0 + - @rocket.chat/ui-contexts@31.0.0-rc.0 + - @rocket.chat/ui-composer@2.0.0-rc.0 + - @rocket.chat/network-broker@0.2.36-rc.0 + - @rocket.chat/ui-avatar@27.0.0-rc.0 + - @rocket.chat/ui-video-conf@31.0.0-rc.0 + +
+ +## 8.4.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. - Bump @rocket.chat/meteor version. - ([#40771](https://github.com/RocketChat/Rocket.Chat/pull/40771) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes the Chat Limits locking mechanism to allow bot agents to skip the lock as they aren't limited @@ -50,6 +247,9 @@ - Bump @rocket.chat/meteor version. +### Patch Changes + +- Bump @rocket.chat/meteor version. - Bump @rocket.chat/meteor version. - ([#40410](https://github.com/RocketChat/Rocket.Chat/pull/40410) by [@dionisio-bot](https://github.com/dionisio-bot)) Disables SAML login when it is set to validate signatures without the proper configuration for it diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts index cfe7b8ab7d4d4..8242b1c66185e 100644 --- a/apps/meteor/app/2fa/server/code/index.ts +++ b/apps/meteor/app/2fa/server/code/index.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { IUser, IMethodConnection } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index dbafa020706f8..1b5bac10a65df 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -102,11 +102,21 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => export const startRestAPI = () => { (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api + .use( + metricsMiddleware({ + basePathRegex: new RegExp(/^\/api\/v1\//), + api: API.v1, + settings, + endpointTimeSummary: metrics.rocketchatRestApi, + endpointTimeHistogram: metrics.rocketchatRestApiSeconds, + responseSizeHistogram: metrics.rocketchatRestApiResponseSizeBytes, + activeRequestsGauge: metrics.rocketchatRestApiActiveRequests, + }), + ) + .use(tracerSpanMiddleware) .use(remoteAddressMiddleware) .use(cors(settings)) .use(loggerMiddleware(logger)) - .use(metricsMiddleware({ basePathRegex: new RegExp(/^\/api\/v1\//), api: API.v1, settings, summary: metrics.rocketchatRestApi })) - .use(tracerSpanMiddleware) .use(API.v1.router) .use(API.default.router).router, ); diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 9898a33ae27af..c728de5b16be5 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage } from 'http'; +import type { IncomingMessage } from 'node:http'; import type { IUser, LicenseModule, RequiredField } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; diff --git a/apps/meteor/app/api/server/lib/MultipartUploadHandler.ts b/apps/meteor/app/api/server/lib/MultipartUploadHandler.ts index 467cf016d0d68..3f375fc4cd053 100644 --- a/apps/meteor/app/api/server/lib/MultipartUploadHandler.ts +++ b/apps/meteor/app/api/server/lib/MultipartUploadHandler.ts @@ -1,8 +1,8 @@ -import fs from 'fs'; -import { IncomingMessage } from 'http'; -import type { Stream, Transform } from 'stream'; -import { Readable } from 'stream'; -import { pipeline } from 'stream/promises'; +import fs from 'node:fs'; +import { IncomingMessage } from 'node:http'; +import type { Stream, Transform } from 'node:stream'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; import { MeteorError } from '@rocket.chat/core-services'; import { Random } from '@rocket.chat/random'; diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 5841a5b58c32b..5e5ae85416d65 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -1,5 +1,5 @@ -import { Readable } from 'stream'; -import { ReadableStream } from 'stream/web'; +import { Readable } from 'node:stream'; +import { ReadableStream } from 'node:stream/web'; import { MeteorError } from '@rocket.chat/core-services'; import type { ValidateFunction } from 'ajv'; @@ -102,7 +102,9 @@ export async function getUploadFormData< return reject(new MeteorError('No file uploaded')); } if (options.validate !== undefined && !options.validate(fields)) { - return reject(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); + return reject( + new MeteorError(`Invalid fields ${(options.validate.errors ?? []).map((e) => `${e.instancePath} ${e.message}`).join(', ')}`), + ); } return resolve(uploadedFile); } diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index d2bac79d6f790..216e17a42422e 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -4,6 +4,7 @@ import type { FindOptions, Sort } from 'mongodb'; import { adminFields } from '../../../../lib/rooms/adminFields'; import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { stripABACManagedFieldsForAdmin } from '../../../authorization/server/lib/isABACManagedRoom'; export async function findAdminRooms({ uid, @@ -39,7 +40,13 @@ export async function findAdminRooms({ const { cursor, totalCount } = result; - const [rooms, total] = await Promise.all([cursor.sort(sort || { default: -1, name: 1 }).toArray(), totalCount]); + const [rooms, total] = await Promise.all([ + cursor + .sort(sort || { default: -1, name: 1 }) + .map(stripABACManagedFieldsForAdmin) + .toArray(), + totalCount, + ]); return { rooms, @@ -54,7 +61,11 @@ export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): throw new Error('error-not-authorized'); } - return Rooms.findOneById(rid, { projection: adminFields }); + const room = await Rooms.findOneById(rid, { projection: adminFields }); + if (!room) { + return null; + } + return stripABACManagedFieldsForAdmin(room); } export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ diff --git a/apps/meteor/app/api/server/middlewares/metrics.spec.ts b/apps/meteor/app/api/server/middlewares/metrics.spec.ts index 6f53a489c6fb5..3fdc94cda826a 100644 --- a/apps/meteor/app/api/server/middlewares/metrics.spec.ts +++ b/apps/meteor/app/api/server/middlewares/metrics.spec.ts @@ -25,27 +25,42 @@ describe('Metrics middleware', () => { const mockEndTimer = jest.fn(); summary.startTimer.mockReturnValue(mockEndTimer); - api.use(metricsMiddleware({ api: { version: 1 } as any, settings, summary: summary as any })).get( - '/test', - { - response: { - 200: ajv.compile({ - type: 'object', - properties: { - message: { type: 'string' }, + const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) }; + const responseSizeHistogram = { observe: jest.fn() }; + const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() }; + + api + .use( + metricsMiddleware({ + api: { version: 1 } as any, + settings, + endpointTimeSummary: summary as any, + endpointTimeHistogram: histogram as any, + responseSizeHistogram: responseSizeHistogram as any, + activeRequestsGauge: activeRequestsGauge as any, + }), + ) + .get( + '/test', + { + response: { + 200: ajv.compile({ + type: 'object', + properties: { + message: { type: 'string' }, + }, + }), + }, + }, + async () => { + return { + statusCode: 200, + body: { + message: 'Metrics test successful', }, - }), + }; }, - }, - async () => { - return { - statusCode: 200, - body: { - message: 'Metrics test successful', - }, - }; - }, - ); + ); app.use(api.router); const response = await request(app).get('/api/test').set('user-agent', 'test'); expect(response.statusCode).toBe(200); @@ -73,8 +88,22 @@ describe('Metrics middleware', () => { const mockEndTimer = jest.fn(); summary.startTimer.mockReturnValue(mockEndTimer); + const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) }; + const responseSizeHistogram = { observe: jest.fn() }; + const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() }; + api - .use(metricsMiddleware({ basePathRegex: new RegExp(/^\/api\//), api: { version: 1 } as any, settings, summary: summary as any })) + .use( + metricsMiddleware({ + basePathRegex: new RegExp(/^\/api\//), + api: { version: 1 } as any, + settings, + endpointTimeSummary: summary as any, + endpointTimeHistogram: histogram as any, + responseSizeHistogram: responseSizeHistogram as any, + activeRequestsGauge: activeRequestsGauge as any, + }), + ) .get( '/test', { @@ -120,8 +149,22 @@ describe('Metrics middleware', () => { const mockEndTimer = jest.fn(); summary.startTimer.mockReturnValue(mockEndTimer); + const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) }; + const responseSizeHistogram = { observe: jest.fn() }; + const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() }; + api - .use(metricsMiddleware({ basePathRegex: new RegExp(/^\/api\//), api: { version: 1 } as any, settings, summary: summary as any })) + .use( + metricsMiddleware({ + basePathRegex: new RegExp(/^\/api\//), + api: { version: 1 } as any, + settings, + endpointTimeSummary: summary as any, + endpointTimeHistogram: histogram as any, + responseSizeHistogram: responseSizeHistogram as any, + activeRequestsGauge: activeRequestsGauge as any, + }), + ) .get( '/method.call/:id', { diff --git a/apps/meteor/app/api/server/middlewares/metrics.ts b/apps/meteor/app/api/server/middlewares/metrics.ts index bedc2cc823a93..75774c495dbaf 100644 --- a/apps/meteor/app/api/server/middlewares/metrics.ts +++ b/apps/meteor/app/api/server/middlewares/metrics.ts @@ -1,5 +1,5 @@ import type { MiddlewareHandler } from 'hono'; -import type { Summary } from 'prom-client'; +import type { Gauge, Histogram, Summary } from 'prom-client'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; import type { APIClass } from '../ApiClass'; @@ -9,28 +9,51 @@ export const metricsMiddleware = basePathRegex, api, settings, - summary, + endpointTimeSummary, + endpointTimeHistogram, + responseSizeHistogram, + activeRequestsGauge, }: { basePathRegex?: RegExp; api: APIClass; settings: CachedSettings; - summary: Summary; + endpointTimeSummary: Summary; + endpointTimeHistogram: Histogram; + responseSizeHistogram: Histogram; + activeRequestsGauge: Gauge; }): MiddlewareHandler => async (c, next) => { - const rocketchatRestApiEnd = summary.startTimer(); + const rocketchatRestApiEnd = endpointTimeSummary.startTimer(); + const rocketchatRestApiHistEnd = endpointTimeHistogram.startTimer(); + + const methodLabel = { method: c.req.method.toLowerCase() }; + activeRequestsGauge.inc(methodLabel); await next(); + activeRequestsGauge.dec(methodLabel); + const { method, path, routePath } = c.req; // get rid of the base path (i.e.: /api/v1/) const entrypoint = basePathRegex ? routePath.replace(basePathRegex, '') : routePath; - rocketchatRestApiEnd({ + const histogramLabels = { status: c.res.status, method: method.toLowerCase(), version: api.version, - ...(settings.get('Prometheus_API_User_Agent') && { user_agent: c.req.header('user-agent') }), entrypoint: basePathRegex && entrypoint.startsWith('method.call') ? decodeURIComponent(path.replace(basePathRegex, '')) : entrypoint, + }; + + rocketchatRestApiEnd({ + ...histogramLabels, + ...(settings.get('Prometheus_API_User_Agent') && { user_agent: c.req.header('user-agent') }), }); + + rocketchatRestApiHistEnd(histogramLabels); + + const contentLength = parseInt(c.res.headers.get('content-length') || '0', 10); + if (contentLength > 0) { + responseSizeHistogram.observe(histogramLabels, contentLength); + } }; diff --git a/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts index c8fff45bae33f..a91bbafbbeb65 100644 --- a/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts +++ b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage } from 'http'; +import type { IncomingMessage } from 'node:http'; import type { Context, MiddlewareHandler } from 'hono'; diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts index 149a8a20a79e0..b74a396c5ee58 100644 --- a/apps/meteor/app/api/server/v1/custom-sounds.ts +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -4,6 +4,8 @@ import type { PaginatedResult } from '@rocket.chat/rest-typings'; import { isCustomSoundsGetOneProps, isCustomSoundsListProps, + isCustomSoundsCreateProps, + isCustomSoundsUpdateProps, ajv, validateBadRequestErrorResponse, validateNotFoundErrorResponse, @@ -12,9 +14,49 @@ import { } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { MAX_CUSTOM_SOUND_SIZE_BYTES, CUSTOM_SOUND_ALLOWED_MIME_TYPES } from '../../../../lib/constants'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { insertOrUpdateSound } from '../../../custom-sounds/server/lib/insertOrUpdateSound'; +import { uploadCustomSound } from '../../../custom-sounds/server/lib/uploadCustomSound'; +import { getExtension, getMimeTypeFromFileName } from '../../../utils/lib/mimeTypes'; import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; +import { getUploadFormData } from '../lib/getUploadFormData'; + +const createCustomSoundsResponse = ajv.compile<{ sound: Pick; success: boolean }>({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + sound: { + type: 'object', + properties: { + _id: { + type: 'string', + description: 'The ID of the sound.', + }, + }, + required: ['_id'], + }, + }, + required: ['success', 'sound'], +}); + +const updateCustomSoundsResponse = ajv.compile<{ success: boolean }>({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], +}); const customSoundsEndpoints = API.v1 .get( @@ -124,6 +166,120 @@ const customSoundsEndpoints = API.v1 return API.v1.success({ sound }); }, + ) + .post( + 'custom-sounds.create', + { + response: { + 200: createCustomSoundsResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + authRequired: true, + permissionsRequired: ['manage-sounds'], + }, + async function action() { + const { fields, fileBuffer, filename } = await getUploadFormData( + { + request: this.request, + }, + { + field: 'sound', + sizeLimit: MAX_CUSTOM_SOUND_SIZE_BYTES, + validate: isCustomSoundsCreateProps, + }, + ); + + const computedMimeType = getMimeTypeFromFileName(filename); + if (!CUSTOM_SOUND_ALLOWED_MIME_TYPES.includes(computedMimeType)) { + return API.v1.failure('MIME type not allowed'); + } + + // We extract its extension from the name or use getExtension() as fallback. + const soundExtension = + (filename.includes('.') ? filename.split('.').pop()?.toLowerCase() : undefined) || getExtension(computedMimeType); + + try { + const _id = await insertOrUpdateSound({ + name: fields.name, + extension: soundExtension, + }); + await uploadCustomSound(fileBuffer, computedMimeType, { _id, name: fields.name, extension: soundExtension }); + return API.v1.success({ sound: { _id } }); + } catch (error) { + SystemLogger.error({ error }); + return API.v1.failure(error instanceof Error ? error.message : 'Unknown error'); + } + }, + ) + .post( + 'custom-sounds.update', + { + response: { + 200: updateCustomSoundsResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + authRequired: true, + permissionsRequired: ['manage-sounds'], + }, + async function action() { + const { fields, fileBuffer, filename } = await getUploadFormData( + { + request: this.request, + }, + { + field: 'sound', + fileOptional: true, + sizeLimit: MAX_CUSTOM_SOUND_SIZE_BYTES, + validate: isCustomSoundsUpdateProps, + }, + ); + + let computedMimeType = ''; + if (fileBuffer) { + computedMimeType = getMimeTypeFromFileName(filename); + if (!CUSTOM_SOUND_ALLOWED_MIME_TYPES.includes(computedMimeType)) { + return API.v1.failure('MIME type not allowed'); + } + } + + const soundToUpdate = await CustomSounds.findOneById>(fields._id, { + projection: { _id: 1, name: 1, extension: 1 }, + }); + if (!soundToUpdate) { + return API.v1.failure('Custom Sound not found.'); + } + + // If new file, we'll extract its extension or use getExtension() as fallback. If there's no new file, we will keep the current extension. + const nextExtension = fileBuffer + ? (filename.includes('.') ? filename.split('.').pop()?.toLowerCase() : undefined) || getExtension(computedMimeType) + : soundToUpdate.extension; + + try { + await insertOrUpdateSound({ + _id: fields._id, + name: fields.name, + extension: nextExtension, + previousName: soundToUpdate.name, + previousExtension: soundToUpdate.extension, + }); + if (fileBuffer) { + await uploadCustomSound(fileBuffer, computedMimeType, { + _id: fields._id, + name: fields.name, + previousExtension: soundToUpdate.extension, + extension: nextExtension, + }); + } + return API.v1.success({}); + } catch (error) { + SystemLogger.error({ error }); + return API.v1.failure(error instanceof Error ? error.message : 'Unknown error'); + } + }, ); export type CustomSoundEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts index b5a6fbd12c36c..0e0ea055160ee 100644 --- a/apps/meteor/app/api/server/v1/invites.ts +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -70,7 +70,6 @@ const invites = API.v1 authRequired: true, response: { 200: ajv.compile({ - additionalProperties: false, type: 'array', items: { $ref: '#/components/schemas/IInvite', diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index c93450d937cd0..cf7a9a2d3d1bf 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { IDirectoryChannelResult, IDirectoryUserResult, IRoom, IUser } from '@rocket.chat/core-typings'; import { Settings, Users, WorkspaceCredentials } from '@rocket.chat/models'; diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 3972a97ee906a..e809baa3d7a19 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -38,10 +38,24 @@ const rolesRoutes = API.v1 { authRequired: true, response: { - 200: ajv.compile<{ roles: IRole[] }>({ + 200: ajv.compile<{ roles: Omit[] }>({ type: 'object', properties: { - roles: { type: 'array', items: { $ref: '#/components/schemas/IRole' } }, + roles: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + description: { type: 'string' }, + mandatory2fa: { type: 'boolean' }, + name: { type: 'string' }, + protected: { type: 'boolean' }, + scope: { type: 'string', enum: ['Users', 'Subscriptions'] }, + }, + required: ['_id', 'description', 'name', 'protected', 'scope'], + }, + }, success: { type: 'boolean', enum: [true] }, }, required: ['roles', 'success'], @@ -187,9 +201,9 @@ const rolesRoutes = API.v1 } const { cursor, totalCount } = await getUsersInRolePaginated(roleData._id, roomId, { - limit: count as number, + limit: count, sort: { username: 1 }, - skip: offset as number, + skip: offset, projection, }); diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 8904d97dfbc93..d927487d9b868 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -59,11 +59,13 @@ import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; import { roomsGetMethod } from '../../../../server/publications/room'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { stripABACManagedFieldsForAdmin } from '../../../authorization/server/lib/isABACManagedRoom'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; import { FileUpload } from '../../../file-upload/server'; import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired'; +import { notifyOnSubscriptionChanged } from '../../../lib/server/lib/notifyListener'; import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom'; import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory'; import { executeGetRoomRoles } from '../../../lib/server/methods/getRoomRoles'; @@ -414,6 +416,54 @@ const roomsSaveNotificationEndpoint = API.v1.post( }, ); +const saveDraftBodySchema = ajv.compile<{ rid: IRoom['_id']; draft: string }>({ + type: 'object', + properties: { + rid: { type: 'string', minLength: 1 }, + draft: { type: 'string' }, + }, + required: ['rid', 'draft'], + additionalProperties: false, +}); + +const saveDraftResponseSchema = ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, +}); + +const roomsSaveDraftEndpoint = API.v1.post( + 'rooms.saveDraft', + { + authRequired: true, + body: saveDraftBodySchema, + response: { + 200: saveDraftResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { rid, draft } = this.bodyParams; + + if (draft.length > (settings.get('Message_MaxAllowedSize') ?? 0)) { + return API.v1.failure('error-message-size-exceeded'); + } + + const subscription = await Subscriptions.updateDraftByRoomIdAndUserId(rid, this.userId, draft || undefined); + if (!subscription) { + throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription'); + } + + void notifyOnSubscriptionChanged(subscription); + + return API.v1.success(); + }, +); + API.v1.post( 'rooms.cleanHistory', { @@ -1432,7 +1482,7 @@ export const roomEndpoints = API.v1 projection: adminFields, }); - const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); + const [rooms, total] = await Promise.all([cursor.map(stripABACManagedFieldsForAdmin).toArray(), totalCount]); return API.v1.success({ rooms, @@ -1630,7 +1680,8 @@ export const roomEndpoints = API.v1 ); type RoomEndpoints = ExtractRoutesFromAPI & ExtractRoutesFromAPI & - ExtractRoutesFromAPI; + ExtractRoutesFromAPI & + ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 2873bc36fc47b..46ec48b96833b 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -59,7 +59,7 @@ import { } from '../../../lib/server/functions/checkUsernameAvailability'; import { deleteUser } from '../../../lib/server/functions/deleteUser'; import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; -import { getFullUserDataByIdOrUsernameOrImportIdOrEmail, defaultFields, fullFields } from '../../../lib/server/functions/getFullUserData'; +import { getFullUserDataByUniqueSearchTerm, defaultFields, fullFields } from '../../../lib/server/functions/getFullUserData'; import { generateUsernameSuggestion } from '../../../lib/server/functions/getUsernameSuggestion'; import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields'; import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation'; @@ -587,22 +587,26 @@ API.v1.get( }, }, async function action() { - const searchTerms: [string, 'id' | 'username' | 'importId' | 'email'] | false = + const searchTerms: [string, 'id' | 'username' | 'importId' | 'email' | 'freeSwitchExtension'] | false = ('userId' in this.queryParams && !!this.queryParams.userId && [this.queryParams.userId, 'id']) || ('username' in this.queryParams && !!this.queryParams.username && [this.queryParams.username, 'username']) || ('importId' in this.queryParams && !!this.queryParams.importId && [this.queryParams.importId, 'importId']) || - ('email' in this.queryParams && !!this.queryParams.email && [this.queryParams.email, 'email']); + ('email' in this.queryParams && !!this.queryParams.email && [this.queryParams.email, 'email']) || + ('freeSwitchExtension' in this.queryParams && + !!this.queryParams.freeSwitchExtension && [this.queryParams.freeSwitchExtension, 'freeSwitchExtension']); if (!searchTerms) { return API.v1.failure('Invalid search query.'); } - const user = await getFullUserDataByIdOrUsernameOrImportIdOrEmail(this.userId, ...searchTerms); + const user = await getFullUserDataByUniqueSearchTerm(this.userId, ...searchTerms); if (!user) { return API.v1.failure('User not found.'); } + const myself = user._id === this.userId; + if (this.queryParams.includeUserRooms === 'true' && (myself || (await hasPermissionAsync(this.userId, 'view-other-user-channels')))) { return API.v1.success({ user: { diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index 399dfd285e65e..b22aeac49b220 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,11 +1,10 @@ import type { IAppServerOrchestrator, AppStatus } from '@rocket.chat/apps'; -import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; -import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; +import type { ProxiedApp } from '@rocket.chat/apps/dist/server/ProxiedApp'; +import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps/dist/server/bridges/AppActivationBridge'; import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; export class AppActivationBridge extends ActivationBridge { - // eslint-disable-next-line no-empty-function constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index f115897c82d82..4e117855615ed 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -1,8 +1,8 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ApiBridge } from '@rocket.chat/apps/dist/server/bridges/ApiBridge'; +import type { AppApi } from '@rocket.chat/apps/dist/server/managers/AppApi'; import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api'; -import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge'; -import type { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi'; import type { Response, Request, IRouter, RequestHandler } from 'express'; import express from 'express'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 3b49cd91394e9..4205c2afa91bc 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -1,4 +1,4 @@ -import { AppBridges } from '@rocket.chat/apps-engine/server/bridges'; +import { AppBridges } from '@rocket.chat/apps/dist/server/bridges/AppBridges'; import { AppActivationBridge } from './activation'; import { AppApisBridge } from './api'; diff --git a/apps/meteor/app/apps/server/bridges/cloud.ts b/apps/meteor/app/apps/server/bridges/cloud.ts index 0f908ccfe0a39..83d9f98311c1b 100644 --- a/apps/meteor/app/apps/server/bridges/cloud.ts +++ b/apps/meteor/app/apps/server/bridges/cloud.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { CloudWorkspaceBridge } from '@rocket.chat/apps/dist/server/bridges/CloudWorkspaceBridge'; import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; -import { CloudWorkspaceBridge } from '@rocket.chat/apps-engine/server/bridges/CloudWorkspaceBridge'; import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 0f378a4a7abc4..36eaaed5f047d 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator, IAppsRoom, IAppsUser } from '@rocket.chat/apps'; +import { CommandBridge } from '@rocket.chat/apps/dist/server/bridges/CommandBridge'; import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; -import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; import type { IMessage, RequiredField, SlashCommand, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { Utilities } from '../../../../ee/lib/misc/Utilities'; diff --git a/apps/meteor/app/apps/server/bridges/contact.ts b/apps/meteor/app/apps/server/bridges/contact.ts index 802b0bb3ec16b..fa00a839c6a4e 100644 --- a/apps/meteor/app/apps/server/bridges/contact.ts +++ b/apps/meteor/app/apps/server/bridges/contact.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ContactBridge } from '@rocket.chat/apps/dist/server/bridges/ContactBridge'; import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; -import { ContactBridge } from '@rocket.chat/apps-engine/server/bridges'; import { addContactEmail } from '../../../livechat/server/lib/contacts/addContactEmail'; import { verifyContactChannel } from '../../../livechat/server/lib/contacts/verifyContactChannel'; diff --git a/apps/meteor/app/apps/server/bridges/details.ts b/apps/meteor/app/apps/server/bridges/details.ts index 3930cdd451cc7..03f25d9f6d05b 100644 --- a/apps/meteor/app/apps/server/bridges/details.ts +++ b/apps/meteor/app/apps/server/bridges/details.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps/dist/server/bridges/AppDetailChangesBridge'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; -import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps-engine/server/bridges/AppDetailChangesBridge'; export class AppDetailChangesBridge extends DetailChangesBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/email.ts b/apps/meteor/app/apps/server/bridges/email.ts index 6d75a45044832..8f984b7bb6ce2 100644 --- a/apps/meteor/app/apps/server/bridges/email.ts +++ b/apps/meteor/app/apps/server/bridges/email.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { EmailBridge } from '@rocket.chat/apps/dist/server/bridges/EmailBridge'; import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; -import { EmailBridge } from '@rocket.chat/apps-engine/server/bridges'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/apps/server/bridges/environmental.ts b/apps/meteor/app/apps/server/bridges/environmental.ts index 705a27186dee7..c79ffbe9f6cf5 100644 --- a/apps/meteor/app/apps/server/bridges/environmental.ts +++ b/apps/meteor/app/apps/server/bridges/environmental.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; -import { EnvironmentalVariableBridge } from '@rocket.chat/apps-engine/server/bridges/EnvironmentalVariableBridge'; +import { EnvironmentalVariableBridge } from '@rocket.chat/apps/dist/server/bridges/EnvironmentalVariableBridge'; export class AppEnvironmentalVariableBridge extends EnvironmentalVariableBridge { allowed: Array; diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index d505a54d5665d..fea16f9cb8a82 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; -import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { ExperimentalBridge } from '@rocket.chat/apps/dist/server/bridges/ExperimentalBridge'; export class AppExperimentalBridge extends ExperimentalBridge { constructor(protected readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index cd297009c4afc..f3bb6c502a426 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps/dist/server/bridges/HttpBridge'; +import { HttpBridge } from '@rocket.chat/apps/dist/server/bridges/HttpBridge'; import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors'; -import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps-engine/server/bridges'; -import { HttpBridge } from '@rocket.chat/apps-engine/server/bridges/HttpBridge'; import { serverFetch as fetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch'; import { censorUrl } from '@rocket.chat/tools'; diff --git a/apps/meteor/app/apps/server/bridges/index.ts b/apps/meteor/app/apps/server/bridges/index.ts index 7705ea45498f0..7ac397768f74d 100644 --- a/apps/meteor/app/apps/server/bridges/index.ts +++ b/apps/meteor/app/apps/server/bridges/index.ts @@ -1,3 +1 @@ -import { RealAppBridges } from './bridges'; - -export { RealAppBridges }; +export * from './bridges'; diff --git a/apps/meteor/app/apps/server/bridges/internal.ts b/apps/meteor/app/apps/server/bridges/internal.ts index 22e44a2bb2d0b..8f5ab68e68787 100644 --- a/apps/meteor/app/apps/server/bridges/internal.ts +++ b/apps/meteor/app/apps/server/bridges/internal.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator, IAppsSetting } from '@rocket.chat/apps'; -import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/InternalBridge'; +import { InternalBridge } from '@rocket.chat/apps/dist/server/bridges/InternalBridge'; import type { ISetting, ISubscription } from '@rocket.chat/core-typings'; import { Settings, Subscriptions } from '@rocket.chat/models'; import { isTruthy } from '@rocket.chat/tools'; diff --git a/apps/meteor/app/apps/server/bridges/internalFederation.ts b/apps/meteor/app/apps/server/bridges/internalFederation.ts index 9bf3b34439ce5..1b221844488e8 100644 --- a/apps/meteor/app/apps/server/bridges/internalFederation.ts +++ b/apps/meteor/app/apps/server/bridges/internalFederation.ts @@ -1,4 +1,4 @@ -import type { IInternalFederationBridge } from '@rocket.chat/apps-engine/server/bridges/IInternalFederationBridge'; +import type { IInternalFederationBridge } from '@rocket.chat/apps/dist/server/bridges/IInternalFederationBridge'; import { FederationKeys } from '@rocket.chat/models'; export class AppInternalFederationBridge implements IInternalFederationBridge { diff --git a/apps/meteor/app/apps/server/bridges/listeners.ts b/apps/meteor/app/apps/server/bridges/listeners.ts index 6c3b97e81f0a4..be8ccf3339205 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.ts +++ b/apps/meteor/app/apps/server/bridges/listeners.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; import type { IAppServerOrchestrator, IAppsRoom, IAppsLivechatRoom, IAppsMessage } from '@rocket.chat/apps'; import type { IPreEmailSentContext } from '@rocket.chat/apps-engine/definition/email'; diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index c73bbda1d3580..81a6d8551f82e 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -1,4 +1,5 @@ -import type { IAppServerOrchestrator, IAppsLivechatMessage, IAppsMessage } from '@rocket.chat/apps'; +import type { IAppServerOrchestrator, IAppsLivechatMessage } from '@rocket.chat/apps'; +import { LivechatBridge } from '@rocket.chat/apps/dist/server/bridges/LivechatBridge'; import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; import type { IVisitorExternalIdentifier, @@ -10,8 +11,7 @@ import type { } from '@rocket.chat/apps-engine/definition/livechat'; import type { IMessage as IAppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; -import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, ILivechatVisitor } from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; import { registerGuest } from '@rocket.chat/omni-core'; @@ -29,12 +29,6 @@ import { online } from '../../../livechat/server/lib/service-status'; import { transfer } from '../../../livechat/server/lib/transfer'; import { settings } from '../../../settings/server'; -declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { - export interface IAppMessagesConverter { - convertMessage(message: IMessage, cacheObj?: object): Promise; - } -} - declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' { interface IExtraRoomParams { customFields?: Record; @@ -65,7 +59,7 @@ export class AppLivechatBridge extends LivechatBridge { // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. const guest = this.orch.getConverters().get('visitors').convertAppVisitor(message.visitor); - const appMessage = (await this.orch.getConverters().get('messages').convertAppMessage(message)) as IMessage | undefined; + const appMessage = await this.orch.getConverters().get('messages').convertAppMessage(message); const livechatMessage = appMessage as ILivechatMessage | undefined; const msg = await sendMessage({ @@ -129,13 +123,12 @@ export class AppLivechatBridge extends LivechatBridge { type: OmnichannelSourceType.APP, id: appId, alias: this.orch.getManager()?.getOneById(appId)?.getName(), - ...(source && - source.type === 'app' && { - sidebarIcon: source.sidebarIcon, - defaultIcon: source.defaultIcon, - label: source.label, - destination: source.destination, - }), + ...(source?.type === 'app' && { + sidebarIcon: source.sidebarIcon, + defaultIcon: source.defaultIcon, + label: source.label, + destination: source.destination, + }), }, }, agent: agentRoom, diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 16366626c07c8..b5be15ae7ca92 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -1,8 +1,8 @@ import type { IAppServerOrchestrator, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; +import type { ITypingDescriptor } from '@rocket.chat/apps/dist/server/bridges/MessageBridge'; +import { MessageBridge } from '@rocket.chat/apps/dist/server/bridges/MessageBridge'; import type { Reaction } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; -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 } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/moderation.ts b/apps/meteor/app/apps/server/bridges/moderation.ts index 0f1e56bbdec35..42d521b102f83 100644 --- a/apps/meteor/app/apps/server/bridges/moderation.ts +++ b/apps/meteor/app/apps/server/bridges/moderation.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ModerationBridge } from '@rocket.chat/apps/dist/server/bridges/ModerationBridge'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { ModerationBridge } from '@rocket.chat/apps-engine/server/bridges/ModerationBridge'; import { ModerationReports } from '@rocket.chat/models'; import { reportMessage } from '../../../../server/lib/moderation/reportMessage'; diff --git a/apps/meteor/app/apps/server/bridges/oauthApps.ts b/apps/meteor/app/apps/server/bridges/oauthApps.ts index bfd72917a367c..d16087cfb5c61 100644 --- a/apps/meteor/app/apps/server/bridges/oauthApps.ts +++ b/apps/meteor/app/apps/server/bridges/oauthApps.ts @@ -1,8 +1,8 @@ -import { randomUUID } from 'crypto'; +import { randomUUID } from 'node:crypto'; import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { OAuthAppsBridge } from '@rocket.chat/apps/dist/server/bridges/OAuthAppsBridge'; import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; -import { OAuthAppsBridge } from '@rocket.chat/apps-engine/server/bridges/OAuthAppsBridge'; import type { IOAuthApps } from '@rocket.chat/core-typings'; import { OAuthApps, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; diff --git a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts index 146ae18b38ead..3ba7218a601c0 100644 --- a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts +++ b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts @@ -1,10 +1,10 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { OutboundMessageBridge } from '@rocket.chat/apps/dist/server/bridges/OutboundMessagesBridge'; import type { IOutboundEmailMessageProvider, IOutboundMessageProviders, IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundCommunication'; -import { OutboundMessageBridge } from '@rocket.chat/apps-engine/server/bridges'; import { getOutboundService } from '../../../livechat/server/lib/outboundcommunication'; diff --git a/apps/meteor/app/apps/server/bridges/persistence.ts b/apps/meteor/app/apps/server/bridges/persistence.ts index e1dedd270ac6e..ce85b801a9808 100644 --- a/apps/meteor/app/apps/server/bridges/persistence.ts +++ b/apps/meteor/app/apps/server/bridges/persistence.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { PersistenceBridge } from '@rocket.chat/apps/dist/server/bridges/PersistenceBridge'; import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; -import { PersistenceBridge } from '@rocket.chat/apps-engine/server/bridges/PersistenceBridge'; import type { InsertOneResult } from 'mongodb'; export class AppPersistenceBridge extends PersistenceBridge { diff --git a/apps/meteor/app/apps/server/bridges/roles.ts b/apps/meteor/app/apps/server/bridges/roles.ts index aa0fcdc7b80b7..7f7daddd328de 100644 --- a/apps/meteor/app/apps/server/bridges/roles.ts +++ b/apps/meteor/app/apps/server/bridges/roles.ts @@ -1,5 +1,5 @@ import type { IAppServerOrchestrator, IAppsRole } from '@rocket.chat/apps'; -import { RoleBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { RoleBridge } from '@rocket.chat/apps/dist/server/bridges/RoleBridge'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 473614edfe25c..11b2b405fb2e6 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,10 +1,10 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps/dist/server/bridges/RoomBridge'; +import { RoomBridge } from '@rocket.chat/apps/dist/server/bridges/RoomBridge'; import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; -import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; import type { FindOptions, Sort } from 'mongodb'; diff --git a/apps/meteor/app/apps/server/bridges/router.ts b/apps/meteor/app/apps/server/bridges/router.ts index 993162b63c16f..3578b311f1c73 100644 --- a/apps/meteor/app/apps/server/bridges/router.ts +++ b/apps/meteor/app/apps/server/bridges/router.ts @@ -5,5 +5,5 @@ export const apiServer = express(); apiServer.disable('x-powered-by'); -apiServer.use('/api/apps/private/:appId/:hash', bodyParser.urlencoded(), bodyParser.json()); -apiServer.use('/api/apps/public/:appId', bodyParser.urlencoded(), bodyParser.json()); +apiServer.use('/api/apps/private/:appId/:hash', bodyParser.urlencoded({ extended: true }), bodyParser.json()); +apiServer.use('/api/apps/public/:appId', bodyParser.urlencoded({ extended: true }), bodyParser.json()); diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index b08d49182c9bc..8f044cf1832f9 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -1,9 +1,9 @@ import type { Job } from '@rocket.chat/agenda'; import { Agenda } from '@rocket.chat/agenda'; import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { SchedulerBridge } from '@rocket.chat/apps/dist/server/bridges/SchedulerBridge'; import type { IProcessor, IOnetimeSchedule, IRecurringSchedule, IJobContext } from '@rocket.chat/apps-engine/definition/scheduler'; import { StartupType } from '@rocket.chat/apps-engine/definition/scheduler'; -import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/SchedulerBridge'; import { ObjectId } from 'bson'; import { MongoInternals } from 'meteor/mongo'; @@ -90,7 +90,7 @@ export class AppSchedulerBridge extends SchedulerBridge { }); if (runAfterRegister.length) { - return Promise.all(runAfterRegister) as Promise>; + return Promise.all(runAfterRegister); } } diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 7c569a3f12fd7..145f81fc129ae 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -1,7 +1,7 @@ import { Apps, type IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ServerSettingBridge } from '@rocket.chat/apps/dist/server/bridges/ServerSettingBridge'; 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'; import { updateAuditedByApp } from '../../../../server/settings/lib/auditedSettingUpdates'; diff --git a/apps/meteor/app/apps/server/bridges/thread.ts b/apps/meteor/app/apps/server/bridges/thread.ts index 099fe9184e004..7857005eaea23 100644 --- a/apps/meteor/app/apps/server/bridges/thread.ts +++ b/apps/meteor/app/apps/server/bridges/thread.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ThreadBridge } from '@rocket.chat/apps/dist/server/bridges/ThreadBridge'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; -import { ThreadBridge } from '@rocket.chat/apps-engine/server/bridges/ThreadBridge'; export class AppThreadBridge extends ThreadBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/uiInteraction.ts b/apps/meteor/app/apps/server/bridges/uiInteraction.ts index fc68e4e30d3ff..69172f9cf5d59 100644 --- a/apps/meteor/app/apps/server/bridges/uiInteraction.ts +++ b/apps/meteor/app/apps/server/bridges/uiInteraction.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps/dist/server/bridges/UiInteractionBridge'; import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps-engine/server/bridges/UiInteractionBridge'; import { api } from '@rocket.chat/core-services'; import type * as UiKit from '@rocket.chat/ui-kit'; diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index b9d0ff67de58e..929e70743c557 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { UploadBridge } from '@rocket.chat/apps/dist/server/bridges/UploadBridge'; import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; -import { UploadBridge } from '@rocket.chat/apps-engine/server/bridges/UploadBridge'; import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; import { FileUpload } from '../../../file-upload/server'; diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 43cd7f9ea89e1..816416fa0002c 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,6 +1,6 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { UserBridge } from '@rocket.chat/apps/dist/server/bridges/UserBridge'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; -import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; import { Presence } from '@rocket.chat/core-services'; import type { UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; @@ -41,7 +41,13 @@ export class AppUserBridge extends UserBridge { return; } - const user = await Users.findOneByAppId(appId, {}); + const user = await Users.findOneByAppId(appId); + + return this.orch.getConverters()?.get('users').convertToApp(user); + } + + protected async getBySipExtension(extension: string, _appId: string): Promise { + const user = await Users.findOneByFreeSwitchExtension(extension); return this.orch.getConverters()?.get('users').convertToApp(user); } diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts index 8986c15a3c6dd..56f70e3e416e4 100644 --- a/apps/meteor/app/apps/server/bridges/videoConferences.ts +++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts @@ -1,7 +1,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { VideoConferenceBridge } from '@rocket.chat/apps/dist/server/bridges/VideoConferenceBridge'; import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; import type { AppVideoConference, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; -import { VideoConferenceBridge } from '@rocket.chat/apps-engine/server/bridges/VideoConferenceBridge'; import { VideoConf } from '@rocket.chat/core-services'; import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index c81792a0bb390..2c699399f1b5e 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -1,3 +1,4 @@ +import { secureFieldsMapper } from '@rocket.chat/apps/dist/lib/SecureFields'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import { LivechatVisitors, Rooms, LivechatDepartment, Users, LivechatContacts } from '@rocket.chat/models'; @@ -435,6 +436,22 @@ export class AppRoomsConverter { return this.orch.getConverters().get('rooms').convertById(prid); }, + ...secureFieldsMapper((room) => { + if (!room.abacAttributes) { + return undefined; + } + + const value = [ + { + permission: 'abac.read', + name: 'abacAttributes', + value: room.abacAttributes, + }, + ]; + + delete room.abacAttributes; + return value; + }), }; return transformMappedData(originalRoom, map); diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index fc560185fc8d5..7ce44b28f8355 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -45,6 +45,7 @@ export class AppUsersConverter { lastLoginAt: user.lastLogin, appId: user.appId, customFields: user.customFields, + sipExtension: user.freeSwitchExtension, settings: { preferences: { ...(user?.settings?.preferences?.language && { language: user.settings.preferences.language }), @@ -74,6 +75,7 @@ export class AppUsersConverter { _updatedAt: user.updatedAt, lastLogin: user.lastLoginAt, appId: user.appId, + freeSwitchExtension: user.sipExtension, }); } diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 9266f21cc0218..c2008c6b8cbe6 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -1,5 +1,5 @@ -import crypto from 'crypto'; -import type { ServerResponse, IncomingMessage } from 'http'; +import crypto from 'node:crypto'; +import type { ServerResponse, IncomingMessage } from 'node:http'; import type { IRocketChatAssets, IRocketChatAsset, ISetting } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; diff --git a/apps/meteor/app/authorization/client/hasPermission.ts b/apps/meteor/app/authorization/client/hasPermission.ts index dd48ce48216c2..0a89b1f9403aa 100644 --- a/apps/meteor/app/authorization/client/hasPermission.ts +++ b/apps/meteor/app/authorization/client/hasPermission.ts @@ -1,81 +1,3 @@ -import type { IUser, IPermission } from '@rocket.chat/core-typings'; +import { liveAuthorizationFunctions } from './liveAuthorizationFunctions'; -import { hasRole } from './hasRole'; -import { PermissionsCachedStore } from '../../../client/cachedStores'; -import { watchUserId } from '../../../client/meteor/user'; -import { watch } from '../../../client/meteor/watch'; -import { Permissions, Users } from '../../../client/stores'; -import { AuthorizationUtils } from '../lib/AuthorizationUtils'; - -const createPermissionValidator = - (quantifier: (predicate: (permissionId: IPermission['_id']) => boolean) => boolean) => - (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id'], scopedRoles?: IPermission['_id'][]): boolean => { - const userRoles = watch(Users.use, (state) => state.get(userId)?.roles); - - const checkEachPermission = quantifier.bind(permissionIds); - - return checkEachPermission((permissionId) => { - if (userRoles) { - if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, userRoles)) { - return false; - } - } - - const permission = watch(Permissions.use, (state) => state.get(permissionId)); - const roles = permission?.roles ?? []; - - return roles.some((roleId) => { - if (scopedRoles?.includes(roleId)) { - return true; - } - - return hasRole(userId, roleId, scope); - }); - }); - }; - -const atLeastOne = createPermissionValidator(Array.prototype.some); - -const all = createPermissionValidator(Array.prototype.every); - -const validatePermissions = ( - permissions: IPermission['_id'] | IPermission['_id'][], - scope: string | undefined, - predicate: ( - permissionIds: IPermission['_id'][], - scope: string | undefined, - userId: IUser['_id'], - scopedRoles?: IPermission['_id'][], - ) => boolean, - userId?: IUser['_id'], - scopedRoles?: IPermission['_id'][], -): boolean => { - userId = userId ?? watchUserId() ?? undefined; - - if (!userId) { - return false; - } - - if (!watch(PermissionsCachedStore.useReady, (state) => state)) { - return false; - } - - return predicate(([] as IPermission['_id'][]).concat(permissions), scope, userId, scopedRoles); -}; - -export const hasAllPermission = ( - permissions: IPermission['_id'] | IPermission['_id'][], - scope?: string, - scopedRoles?: IPermission['_id'][], -): boolean => validatePermissions(permissions, scope, all, undefined, scopedRoles); - -export const hasAtLeastOnePermission = (permissions: IPermission['_id'] | IPermission['_id'][], scope?: string): boolean => - validatePermissions(permissions, scope, atLeastOne); - -export const userHasAllPermission = ( - permissions: IPermission['_id'] | IPermission['_id'][], - scope?: string, - userId?: IUser['_id'], -): boolean => validatePermissions(permissions, scope, all, userId); - -export const hasPermission = hasAllPermission; +export const { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } = liveAuthorizationFunctions; diff --git a/apps/meteor/app/authorization/client/hasRole.ts b/apps/meteor/app/authorization/client/hasRole.ts index a2e1ea5adc229..2c0879d504163 100644 --- a/apps/meteor/app/authorization/client/hasRole.ts +++ b/apps/meteor/app/authorization/client/hasRole.ts @@ -1,21 +1,3 @@ -import type { IUser, IRole, IRoom } from '@rocket.chat/core-typings'; +import { liveAuthorizationFunctions } from './liveAuthorizationFunctions'; -import { watch } from '../../../client/meteor/watch'; -import { Roles, Subscriptions, Users } from '../../../client/stores'; - -export const hasRole = (userId: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): boolean => { - const roleScope = watch(Roles.use, (state) => state.get(roleId)?.scope ?? 'Users'); - - switch (roleScope) { - case 'Subscriptions': - if (!scope) return false; - - return watch(Subscriptions.use, (state) => state.find((record) => record.rid === scope)?.roles?.includes(roleId) ?? false); - - case 'Users': - return watch(Users.use, (state) => state.get(userId)?.roles?.includes(roleId) ?? false); - - default: - return false; - } -}; +export const { hasRole } = liveAuthorizationFunctions; diff --git a/apps/meteor/app/authorization/client/liveAuthorizationFunctions.ts b/apps/meteor/app/authorization/client/liveAuthorizationFunctions.ts new file mode 100644 index 0000000000000..db1981a884a9f --- /dev/null +++ b/apps/meteor/app/authorization/client/liveAuthorizationFunctions.ts @@ -0,0 +1,25 @@ +import { PermissionsCachedStore } from '../../../client/cachedStores'; +import { userIdStore } from '../../../client/lib/user'; +import { Permissions, Roles, Subscriptions, Users } from '../../../client/stores'; +import type { AuthorizationDeps } from '../lib/createAuthorizationFunctions'; +import { createAuthorizationFunctions } from '../lib/createAuthorizationFunctions'; + +// Bind the pure factory to live zustand store accessors. Each accessor reads +// fresh state on every call, so non-React callers (services, lib code, startup +// scripts) keep their previous "always reflects the current store" contract +// without going through Meteor's Tracker. React consumers should use the +// AuthorizationContext instead, which injects React-reactive snapshots. +const liveDeps: AuthorizationDeps = { + getCurrentUserId: () => userIdStore.getState(), + getUserRoles: (userId) => Users.use.getState().get(userId)?.roles, + getPermission: (permissionId) => Permissions.use.getState().get(permissionId), + getRoleScope: (roleId) => Roles.use.getState().get(roleId)?.scope, + hasSubscriptionRole: (rid, roleId) => + Subscriptions.use + .getState() + .find((s) => s.rid === rid) + ?.roles?.includes(roleId) ?? false, + isReady: () => PermissionsCachedStore.useReady.getState(), +}; + +export const liveAuthorizationFunctions = createAuthorizationFunctions(liveDeps); diff --git a/apps/meteor/app/authorization/lib/createAuthorizationFunctions.ts b/apps/meteor/app/authorization/lib/createAuthorizationFunctions.ts new file mode 100644 index 0000000000000..1b689e176aa1b --- /dev/null +++ b/apps/meteor/app/authorization/lib/createAuthorizationFunctions.ts @@ -0,0 +1,101 @@ +import type { IPermission, IRole, IUser } from '@rocket.chat/core-typings'; + +import { AuthorizationUtils } from './AuthorizationUtils'; + +export type AuthorizationDeps = { + /** The currently logged-in user id, or undefined. */ + getCurrentUserId: () => IUser['_id'] | undefined; + /** The role ids assigned to a given user (Users scope). */ + getUserRoles: (userId: IUser['_id']) => IRole['_id'][] | undefined; + /** Lookup a permission by id. */ + getPermission: (permissionId: IPermission['_id']) => IPermission | undefined; + /** The scope of a role; defaults to 'Users' when the role is unknown. */ + getRoleScope: (roleId: IRole['_id']) => IRole['scope'] | undefined; + /** Whether a subscription scoped to `rid` grants `roleId`. */ + hasSubscriptionRole: (rid: string, roleId: IRole['_id']) => boolean; + /** Whether the permissions cache is hydrated; otherwise checks short-circuit to false. */ + isReady: () => boolean; +}; + +export type AuthorizationFunctions = { + hasRole: (userId: IUser['_id'], roleId: IRole['_id'], scope?: string) => boolean; + hasAllPermission: (permissions: IPermission['_id'] | IPermission['_id'][], scope?: string, scopedRoles?: IRole['_id'][]) => boolean; + hasAtLeastOnePermission: (permissions: IPermission['_id'] | IPermission['_id'][], scope?: string) => boolean; + /** Alias of hasAllPermission, kept for parity with the previous API. */ + hasPermission: (permissions: IPermission['_id'] | IPermission['_id'][], scope?: string, scopedRoles?: IRole['_id'][]) => boolean; + userHasAllPermission: ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope: string | undefined, + userId: IUser['_id'], + ) => boolean; +}; + +/** + * Pure factory for the client-side authorization helpers. All store access is + * threaded through the {@link AuthorizationDeps} accessors, so the returned + * functions are testable in isolation and reusable across any state backend. + */ +export const createAuthorizationFunctions = (deps: AuthorizationDeps): AuthorizationFunctions => { + const hasRole = (userId: IUser['_id'], roleId: IRole['_id'], scope?: string): boolean => { + const roleScope = deps.getRoleScope(roleId) ?? 'Users'; + switch (roleScope) { + case 'Subscriptions': + if (!scope) return false; + return deps.hasSubscriptionRole(scope, roleId); + case 'Users': + return deps.getUserRoles(userId)?.includes(roleId) ?? false; + default: + return false; + } + }; + + const checkPermissions = ( + permissionIds: IPermission['_id'][], + scope: string | undefined, + userId: IUser['_id'], + scopedRoles: IRole['_id'][] | undefined, + quantifier: (this: IPermission['_id'][], predicate: (id: IPermission['_id']) => boolean) => boolean, + ): boolean => { + const userRoles = deps.getUserRoles(userId); + return quantifier.call(permissionIds, (permissionId) => { + if (userRoles && AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, userRoles)) { + return false; + } + const roles = deps.getPermission(permissionId)?.roles ?? []; + return roles.some((roleId) => { + if (scopedRoles?.includes(roleId)) return true; + return hasRole(userId, roleId, scope); + }); + }); + }; + + const validatePermissions = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope: string | undefined, + quantifier: (this: IPermission['_id'][], predicate: (id: IPermission['_id']) => boolean) => boolean, + userId: IUser['_id'] | undefined, + scopedRoles?: IRole['_id'][], + ): boolean => { + if (!userId) return false; + if (!deps.isReady()) return false; + const ids = ([] as IPermission['_id'][]).concat(permissions); + return checkPermissions(ids, scope, userId, scopedRoles, quantifier); + }; + + const hasAllPermission: AuthorizationFunctions['hasAllPermission'] = (permissions, scope, scopedRoles) => + validatePermissions(permissions, scope, Array.prototype.every, deps.getCurrentUserId(), scopedRoles); + + const hasAtLeastOnePermission: AuthorizationFunctions['hasAtLeastOnePermission'] = (permissions, scope) => + validatePermissions(permissions, scope, Array.prototype.some, deps.getCurrentUserId()); + + const userHasAllPermission: AuthorizationFunctions['userHasAllPermission'] = (permissions, scope, userId) => + validatePermissions(permissions, scope, Array.prototype.every, userId); + + return { + hasRole, + hasAllPermission, + hasAtLeastOnePermission, + hasPermission: hasAllPermission, + userHasAllPermission, + }; +}; diff --git a/apps/meteor/app/authorization/server/lib/isABACManagedRoom.ts b/apps/meteor/app/authorization/server/lib/isABACManagedRoom.ts new file mode 100644 index 0000000000000..70a5b61f0a375 --- /dev/null +++ b/apps/meteor/app/authorization/server/lib/isABACManagedRoom.ts @@ -0,0 +1,14 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import { settings } from '../../../settings/server'; + +export const isABACManagedRoom = (room: Pick): boolean => + room.t === 'p' && settings.get('ABAC_Enabled') && Array.isArray(room.abacAttributes) && room.abacAttributes.length > 0; + +export const stripABACManagedFieldsForAdmin = >(room: T): T => { + if (!isABACManagedRoom(room)) { + return room; + } + const { announcement, topic, description, ...rest } = room as T & Pick; + return rest as T; +}; diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 742ea4c9a5b79..4b12d7ec0017e 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -1,7 +1,3 @@ -import { Meteor } from 'meteor/meteor'; - import { AutoTranslate } from './autotranslate'; -Meteor.startup(() => { - AutoTranslate.init(); -}); +AutoTranslate.init(); diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 1c294c2023137..9ab66e6086efc 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -1,13 +1,11 @@ import type { IRoom, ISubscription, ISupportedLanguage, ITranslatedMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings'; import { isTranslatedMessageAttachment } from '@rocket.chat/core-typings'; import mem from 'mem'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; +import { PermissionsCachedStore } from '../../../../client/cachedStores'; import { settings } from '../../../../client/lib/settings'; -import { getUserId } from '../../../../client/lib/user'; -import { watchUser, watchUserId } from '../../../../client/meteor/user'; -import { Messages, Subscriptions } from '../../../../client/stores'; +import { getUserId, userIdStore } from '../../../../client/lib/user'; +import { Messages, Subscriptions, Users } from '../../../../client/stores'; import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage, @@ -18,15 +16,16 @@ import { sdk } from '../../../utils/client/lib/SDKClient'; let userLanguage = 'en'; let username = ''; -Meteor.startup(() => { - Tracker.autorun(() => { - const user = watchUser(); - if (!user) return; - - userLanguage = user.language || 'en'; - username = user.username || ''; - }); -}); +const refreshUserCache = () => { + const uid = userIdStore.getState(); + const user = uid ? Users.use.getState().get(uid) : undefined; + if (!user) return; + userLanguage = user.language || 'en'; + username = user.username || ''; +}; +refreshUserCache(); +userIdStore.subscribe(refreshUserCache); +Users.use.subscribe(refreshUserCache); export const AutoTranslate = { initialized: false, @@ -84,14 +83,7 @@ export const AutoTranslate = { return; } - Tracker.autorun(async (c) => { - const uid = watchUserId(); - if (!settings.watch('AutoTranslate_Enabled') || !uid || !hasPermission('auto-translate')) { - return; - } - - c.stop(); - + const loadProviders = async () => { try { [this.providersMetadata, this.supportedLanguages] = await Promise.all([ sdk.call('autoTranslate.getProviderUiMetadata'), @@ -101,7 +93,25 @@ export const AutoTranslate = { // Avoid unwanted error message on UI when autotranslate is disabled while fetching data console.error((e as Error).message); } - }); + }; + + let loaded = false; + const unsubs: Array<() => void> = []; + const tryLoad = async () => { + if (loaded) return; + if (!settings.peek('AutoTranslate_Enabled') || !userIdStore.getState() || !hasPermission('auto-translate')) { + return; + } + loaded = true; + unsubs.splice(0).forEach((unsubscribe) => unsubscribe()); + await loadProviders(); + }; + + unsubs.push(userIdStore.subscribe(() => void tryLoad())); + unsubs.push(settings.observe('AutoTranslate_Enabled', () => void tryLoad())); + unsubs.push(PermissionsCachedStore.useReady.subscribe(() => void tryLoad())); + + void tryLoad(); Subscriptions.use.subscribe(() => { mem.clear(this.findSubscriptionByRid); diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts index 4600d1d46a805..1f0ad3f29192c 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts @@ -48,6 +48,10 @@ export const saveRoomType = async function ( return result; } + if (roomType === 'c' && room.abacAttributes?.length) { + await Rooms.unsetAbacAttributesById(rid); + } + if (result[1]?.modifiedCount) { void notifyOnSubscriptionChangedByRoomId(rid); } diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 0a9b282160619..158be6b8b946e 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -9,6 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { isABACManagedRoom } from '../../../authorization/server/lib/isABACManagedRoom'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; @@ -62,10 +63,6 @@ type RoomSettingsValidators = { const hasRetentionPolicy = (room: IRoom & { retention?: any }): room is IRoomWithRetentionPolicy => 'retention' in room && room.retention !== undefined; -const isAbacManagedRoom = (room: IRoom): boolean => { - return room.t === 'p' && settings.get('ABAC_Enabled') && Array.isArray(room?.abacAttributes) && room.abacAttributes.length > 0; -}; - const isAbacManagedTeam = (team: Partial | null, teamRoom: IRoom): boolean => { return ( team?.type === TeamType.PRIVATE && @@ -75,6 +72,21 @@ const isAbacManagedTeam = (team: Partial | null, teamRoom: IRoom): boolea ); }; +const guardABACManagedField = (room: IRoom, value: string | undefined, current: string | undefined, fieldName: string): void => { + if (!value && !current) { + return; + } + if (value === current) { + return; + } + if (isABACManagedRoom(room)) { + throw new Meteor.Error('error-action-not-allowed', `Editing an ABAC managed room's ${fieldName} is not allowed`, { + method: 'saveRoomSettings', + action: 'Editing_room', + }); + } +}; + const validators: RoomSettingsValidators = { async default({ userId, room, value }) { if (!(await hasPermissionAsync(userId, 'view-room-administration'))) { @@ -83,7 +95,7 @@ const validators: RoomSettingsValidators = { action: 'Viewing_room_administration', }); } - if (isAbacManagedRoom(room) && value) { + if (isABACManagedRoom(room) && value) { throw new Meteor.Error('error-action-not-allowed', 'Setting an ABAC managed room as default is not allowed', { method: 'saveRoomSettings', action: 'Viewing_room_administration', @@ -118,7 +130,7 @@ const validators: RoomSettingsValidators = { }); } - if (isAbacManagedRoom(room) && value !== 'p') { + if (isABACManagedRoom(room) && value !== 'p') { throw new Meteor.Error('error-action-not-allowed', 'Changing an ABAC managed private room to public is not allowed', { method: 'saveRoomSettings', action: 'Change_Room_Type', @@ -151,6 +163,15 @@ const validators: RoomSettingsValidators = { }); } }, + async roomAnnouncement({ room, value }) { + guardABACManagedField(room, value, room.announcement, 'announcement'); + }, + async roomTopic({ room, value }) { + guardABACManagedField(room, value, room.topic, 'topic'); + }, + async roomDescription({ room, value }) { + guardABACManagedField(room, value, room.description, 'description'); + }, async encrypted({ userId, value, room, rid }) { if (value !== room.encrypted) { if (!(await roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange(room, RoomSettingsEnum.E2E))) { diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts index 4354be6d5bba0..a7617a5465fde 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts @@ -1,5 +1,5 @@ import { NPS, Banner } from '@rocket.chat/core-services'; -import type { Cloud, IBanner } from '@rocket.chat/core-typings'; +import type { Cloud, IBanner, Optional } from '@rocket.chat/core-typings'; import { getAndCreateNpsSurvey } from '../../../../../server/services/nps/getAndCreateNpsSurvey'; @@ -23,7 +23,7 @@ export const handleNpsOnWorkspaceSync = async (nps: Cloud.NpsSurveyAnnouncement) } }; -export const handleBannerOnWorkspaceSync = async (banners: IBanner[]) => { +export const handleBannerOnWorkspaceSync = async (banners: Optional[]) => { for (const banner of banners) { await Banner.create(banner); } diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 5e7909c525dfb..9464762d57d04 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -1,6 +1,6 @@ -import type http from 'http'; -import type { UrlWithParsedQuery } from 'url'; -import url from 'url'; +import type http from 'node:http'; +import type { UrlWithParsedQuery } from 'node:url'; +import url from 'node:url'; import { Logger } from '@rocket.chat/logger'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts new file mode 100644 index 0000000000000..856852106eba6 --- /dev/null +++ b/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts @@ -0,0 +1,61 @@ +import { api } from '@rocket.chat/core-services'; +import { CustomSounds } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; +import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; + +export const insertOrUpdateSound = async (soundData: ICustomSoundData): Promise => { + // silently strip colon; this allows for uploading :soundname: as soundname + soundData.name = (soundData.name || '').replace(/:/g, ''); + + if (!soundData.name.trim()) { + throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { + method: 'insertOrUpdateSound', + field: 'Name', + }); + } + + // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, ) + // more practical than allowing specific sets of characters; also allows foreign languages + const nameValidation = /[\s,:><&"'\/\\\(\)]/; + + if (nameValidation.test(soundData.name)) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${soundData.name} is not a valid name`, { + method: 'insertOrUpdateSound', + input: soundData.name, + field: 'Name', + }); + } + + const matchingResults = await CustomSounds.findByName(soundData.name, soundData._id).toArray(); + + if (matchingResults.length > 0) { + throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', { + method: 'insertOrUpdateSound', + }); + } + + if (!soundData._id) { + return ( + await CustomSounds.create({ + name: soundData.name, + extension: soundData.extension, + }) + ).insertedId; + } + + if (soundData.newFile) { + await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`); + } + + if (soundData.name !== soundData.previousName || soundData.extension !== soundData.previousExtension) { + await CustomSounds.updateById(soundData._id, { name: soundData.name, extension: soundData.extension }); + const updatedSound = await CustomSounds.findOneById(soundData._id); + if (updatedSound) { + void api.broadcast('notify.updateCustomSound', { soundData: updatedSound }); + } + } + + return soundData._id; +}; diff --git a/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts new file mode 100644 index 0000000000000..926b5a2fed540 --- /dev/null +++ b/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts @@ -0,0 +1,44 @@ +import { api } from '@rocket.chat/core-services'; +import type { RequiredField } from '@rocket.chat/core-typings'; +import { CustomSounds } from '@rocket.chat/models'; + +import { RocketChatFile } from '../../../file/server'; +import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; +import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; + +export const uploadCustomSound = async ( + buffer: Buffer, + contentType: string, + soundData: RequiredField, +): Promise => { + const rs = RocketChatFile.bufferToStream(buffer); + + if (soundData.previousExtension) { + await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`); + } + + return new Promise((resolve, reject) => { + const ws = RocketChatFileCustomSoundsInstance.createWriteStream(`${soundData._id}.${soundData.extension}`, contentType); + + ws.on('error', (err: Error) => { + reject(err); + }); + + rs.on('error', (err: Error) => { + ws.destroy(); + reject(err); + }); + + ws.on('end', () => { + setTimeout(async () => { + const sound = await CustomSounds.findOneById(soundData._id); + if (sound) { + void api.broadcast('notify.updateCustomSound', { soundData: sound }); + } + }, 500); + resolve(); + }); + + rs.pipe(ws); + }); +}; diff --git a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts index 1b922c6b162ed..a2881ad13ef2d 100644 --- a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts +++ b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts @@ -1,11 +1,10 @@ -import { api } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { CustomSounds } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { insertOrUpdateSound } from '../lib/insertOrUpdateSound'; export type ICustomSoundData = { _id?: string; @@ -29,74 +28,13 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async insertOrUpdateSound(soundData) { + methodDeprecationLogger.method('insertOrUpdateSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { throw new Meteor.Error('not_authorized'); } - - if (!soundData.name?.trim()) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateSound', - field: 'Name', - }); - } - - // let nameValidation = new RegExp('^[0-9a-zA-Z-_+;.]+$'); - - // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, ) - // more practical than allowing specific sets of characters; also allows foreign languages - const nameValidation = /[\s,:><&"'\/\\\(\)]/; - - // silently strip colon; this allows for uploading :soundname: as soundname - soundData.name = soundData.name.replace(/:/g, ''); - - if (nameValidation.test(soundData.name)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${soundData.name} is not a valid name`, { - method: 'insertOrUpdateSound', - input: soundData.name, - field: 'Name', - }); - } - - let matchingResults = []; - if (soundData._id) { check(soundData._id, String); - matchingResults = await CustomSounds.findByNameExceptId(soundData.name, soundData._id).toArray(); - } else { - matchingResults = await CustomSounds.findByName(soundData.name).toArray(); } - - if (matchingResults.length > 0) { - throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', { - method: 'insertOrUpdateSound', - }); - } - - if (!soundData._id) { - return ( - await CustomSounds.create({ - name: soundData.name, - extension: soundData.extension, - }) - ).insertedId; - } - - // update sound - if (soundData.newFile) { - await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`); - } - - if (soundData.name !== soundData.previousName) { - await CustomSounds.setName(soundData._id, soundData.name); - void api.broadcast('notify.updateCustomSound', { - soundData: { - _id: soundData._id, - name: soundData.name, - extension: soundData.extension, - }, - }); - } - - return soundData._id; + return insertOrUpdateSound(soundData); }, }); diff --git a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts index 64286bb71d86a..e979d66110ba9 100644 --- a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts +++ b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts @@ -1,12 +1,11 @@ -import { api } from '@rocket.chat/core-services'; import type { RequiredField } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import type { ICustomSoundData } from './insertOrUpdateSound'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFile } from '../../../file/server'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { uploadCustomSound } from '../lib/uploadCustomSound'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -17,23 +16,11 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async uploadCustomSound(binaryContent, contentType, soundData) { + methodDeprecationLogger.method('uploadCustomSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { throw new Meteor.Error('not_authorized'); } - - const file = Buffer.from(binaryContent, 'binary'); - - const rs = RocketChatFile.bufferToStream(file); - await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.extension}`); - - return new Promise((resolve) => { - const ws = RocketChatFileCustomSoundsInstance.createWriteStream(`${soundData._id}.${soundData.extension}`, contentType); - ws.on('end', () => { - setTimeout(() => api.broadcast('notify.updateCustomSound', { soundData }), 500); - resolve(); - }); - - rs.pipe(ws); - }); + const buffer = Buffer.from(binaryContent, 'binary'); + await uploadCustomSound(buffer, contentType, soundData); }, }); diff --git a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs b/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs index 009ef3dcc8f47..fbed7b93cd9b1 100644 --- a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs +++ b/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs @@ -3,7 +3,7 @@ // before using this script make sure to run: npm i --no-save node-sprite-generator // node --experimental-modules generateEmojiIndex.mjs -import fs from 'fs'; +import fs from 'node:fs'; import nsg from 'node-sprite-generator'; import _ from 'underscore'; import gm from 'gm'; // lgtm[js/unused-local-variable] diff --git a/apps/meteor/app/file-upload/lib/FileUploadBase.ts b/apps/meteor/app/file-upload/lib/FileUploadBase.ts index e6027c833e5da..a9dc82ef46e57 100644 --- a/apps/meteor/app/file-upload/lib/FileUploadBase.ts +++ b/apps/meteor/app/file-upload/lib/FileUploadBase.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; import { UploadFS } from '../../../server/ufs'; diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 36da7e5feeee9..77a554e46b7af 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -1,5 +1,5 @@ -import http from 'http'; -import https from 'https'; +import http from 'node:http'; +import https from 'node:https'; import _ from 'underscore'; diff --git a/apps/meteor/app/file-upload/server/config/FileSystem.ts b/apps/meteor/app/file-upload/server/config/FileSystem.ts index c39cf5807d602..512b2dd175f20 100644 --- a/apps/meteor/app/file-upload/server/config/FileSystem.ts +++ b/apps/meteor/app/file-upload/server/config/FileSystem.ts @@ -1,4 +1,4 @@ -import fsp from 'fs/promises'; +import fsp from 'node:fs/promises'; import { getContentDisposition } from './helper'; import { UploadFS } from '../../../../server/ufs'; diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index ccd94d533b62c..fface3fce7d2d 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -1,5 +1,5 @@ -import http from 'http'; -import https from 'https'; +import http from 'node:http'; +import https from 'node:https'; import _ from 'underscore'; @@ -68,7 +68,7 @@ const configure = _.debounce(() => { const projectId = settings.get('FileUpload_GoogleStorage_ProjectId'); const accessId = settings.get('FileUpload_GoogleStorage_AccessId'); const secret = settings.get('FileUpload_GoogleStorage_Secret'); - const URLExpiryTimeSpan = settings.get('FileUpload_S3_URLExpiryTimeSpan'); + const URLExpiryTimeSpan = settings.get('FileUpload_GoogleStorage_URLExpiryTimeSpan'); if (!bucket || !accessId || !secret) { return; diff --git a/apps/meteor/app/file-upload/server/config/GridFS.ts b/apps/meteor/app/file-upload/server/config/GridFS.ts index bb9dfa5891f26..94c6629e4f245 100644 --- a/apps/meteor/app/file-upload/server/config/GridFS.ts +++ b/apps/meteor/app/file-upload/server/config/GridFS.ts @@ -1,7 +1,7 @@ -import type * as http from 'http'; -import type { TransformCallback, TransformOptions } from 'stream'; -import stream from 'stream'; -import zlib from 'zlib'; +import type * as http from 'node:http'; +import type { TransformCallback, TransformOptions } from 'node:stream'; +import stream from 'node:stream'; +import zlib from 'node:zlib'; import type { IUpload } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; diff --git a/apps/meteor/app/file-upload/server/config/helper.ts b/apps/meteor/app/file-upload/server/config/helper.ts index f1c465537255c..df07a2ac41bf4 100644 --- a/apps/meteor/app/file-upload/server/config/helper.ts +++ b/apps/meteor/app/file-upload/server/config/helper.ts @@ -1,5 +1,5 @@ -import type http from 'http'; -import URL from 'url'; +import type http from 'node:http'; +import URL from 'node:url'; export const forceDownload = (req: http.IncomingMessage): boolean => { const { query } = URL.parse(req.url || '', true); diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index a368d28586c0c..43dfc089e2e4f 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -1,13 +1,13 @@ -import { Buffer } from 'buffer'; -import type { WriteStream } from 'fs'; -import fs from 'fs'; -import { unlink, rename, writeFile } from 'fs/promises'; -import type * as http from 'http'; -import type * as https from 'https'; -import stream from 'stream'; -import { finished } from 'stream/promises'; -import URL from 'url'; -import { isArrayBufferView } from 'util/types'; +import { Buffer } from 'node:buffer'; +import type { WriteStream } from 'node:fs'; +import fs from 'node:fs'; +import { unlink, rename, writeFile } from 'node:fs/promises'; +import type * as http from 'node:http'; +import type * as https from 'node:https'; +import stream from 'node:stream'; +import { finished } from 'node:stream/promises'; +import URL from 'node:url'; +import { isArrayBufferView } from 'node:util/types'; import { hashLoginToken } from '@rocket.chat/account-utils'; import { Apps, AppEvents } from '@rocket.chat/apps'; @@ -284,7 +284,7 @@ export const FileUpload = { ); }, - async resizeImagePreview(fileParam: IUpload) { + async resizeImagePreview(fileParam: Pick) { let file = await Uploads.findOneById(fileParam._id); if (!file) { return; @@ -302,7 +302,7 @@ export const FileUpload = { return sharp(await FileUpload.getBuffer(file)).metadata(); }, - async createImageThumbnail(fileParam: IUpload) { + async createImageThumbnail(fileParam: Pick) { if (!settings.get('Message_Attachments_Thumbnails_Enabled')) { return; } @@ -836,7 +836,7 @@ export class FileUploadClass { } private async _validateFile( - fileData: OptionalId, + fileData: Omit, '_updatedAt'>, content: stream.Readable | Buffer | string, ): Promise { const filter = this.store.getFilter(); @@ -863,7 +863,7 @@ export class FileUploadClass { } async _doInsert( - fileData: OptionalId, + fileData: Omit, '_updatedAt'>, content: stream.Readable | Buffer | string, options?: { session?: ClientSession }, ): Promise { @@ -888,7 +888,7 @@ export class FileUploadClass { } async insert( - fileData: OptionalId, + fileData: Omit, '_updatedAt'>, streamOrBuffer: stream.Readable | Buffer | string, options?: { session?: ClientSession }, ): Promise { diff --git a/apps/meteor/app/file-upload/server/lib/ranges.ts b/apps/meteor/app/file-upload/server/lib/ranges.ts index 0ac86347ca423..0fedb0e981fac 100644 --- a/apps/meteor/app/file-upload/server/lib/ranges.ts +++ b/apps/meteor/app/file-upload/server/lib/ranges.ts @@ -1,4 +1,4 @@ -import type http from 'http'; +import type http from 'node:http'; import type { IUpload } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/app/file-upload/server/lib/requests.ts b/apps/meteor/app/file-upload/server/lib/requests.ts index b2ae48fbca362..22799b5cb5fae 100644 --- a/apps/meteor/app/file-upload/server/lib/requests.ts +++ b/apps/meteor/app/file-upload/server/lib/requests.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage } from 'http'; +import type { IncomingMessage } from 'node:http'; import { Uploads } from '@rocket.chat/models'; import { WebApp } from 'meteor/webapp'; diff --git a/apps/meteor/app/file-upload/server/lib/urlExpiry.spec.ts b/apps/meteor/app/file-upload/server/lib/urlExpiry.spec.ts new file mode 100644 index 0000000000000..69a4da82730df --- /dev/null +++ b/apps/meteor/app/file-upload/server/lib/urlExpiry.spec.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { MIN_URL_EXPIRY_TIME_SPAN_SECONDS, URL_EXPIRY_FALLBACK_SECONDS, getUrlExpiryTimeSpanWithFallback } from './urlExpiry'; + +describe('getUrlExpiryTimeSpanWithFallback', () => { + it('uses fallback when configured value is below minimum', () => { + expect(getUrlExpiryTimeSpanWithFallback(0)).to.equal(URL_EXPIRY_FALLBACK_SECONDS); + expect(getUrlExpiryTimeSpanWithFallback(3)).to.equal(URL_EXPIRY_FALLBACK_SECONDS); + }); + + it('uses configured value when at or above minimum', () => { + expect(getUrlExpiryTimeSpanWithFallback(MIN_URL_EXPIRY_TIME_SPAN_SECONDS)).to.equal(MIN_URL_EXPIRY_TIME_SPAN_SECONDS); + expect(getUrlExpiryTimeSpanWithFallback(300)).to.equal(300); + }); +}); diff --git a/apps/meteor/app/file-upload/server/lib/urlExpiry.ts b/apps/meteor/app/file-upload/server/lib/urlExpiry.ts new file mode 100644 index 0000000000000..2d6336b6ef3d1 --- /dev/null +++ b/apps/meteor/app/file-upload/server/lib/urlExpiry.ts @@ -0,0 +1,6 @@ +export const MIN_URL_EXPIRY_TIME_SPAN_SECONDS = 5; +export const URL_EXPIRY_FALLBACK_SECONDS = 900; + +export const getUrlExpiryTimeSpanWithFallback = (configuredValue: number): number => { + return configuredValue >= MIN_URL_EXPIRY_TIME_SPAN_SECONDS ? configuredValue : URL_EXPIRY_FALLBACK_SECONDS; +}; diff --git a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts index b223d879c78bb..2ab3312b31f30 100644 --- a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts +++ b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts @@ -1,4 +1,4 @@ -import stream from 'stream'; +import stream from 'node:stream'; import { DeleteObjectCommand, @@ -13,12 +13,12 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import type { IUpload } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { check } from 'meteor/check'; -import type { OptionalId } from 'mongodb'; import _ from 'underscore'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { UploadFS } from '../../../../server/ufs'; import type { StoreOptions } from '../../../../server/ufs/ufs-store'; +import { getUrlExpiryTimeSpanWithFallback } from '../../server/lib/urlExpiry'; export type S3Options = StoreOptions & { connection: S3ClientConfig; @@ -27,11 +27,11 @@ export type S3Options = StoreOptions & { ACL: string; }; URLExpiryTimeSpan: number; - getPath: (file: OptionalId) => string; + getPath: (file: Omit) => string; }; class AmazonS3Store extends UploadFS.Store { - protected getPath: (file: IUpload) => string; + protected getPath: (file: Omit) => string; constructor(options: S3Options) { // Default options @@ -81,6 +81,7 @@ class AmazonS3Store extends UploadFS.Store { }; this.getRedirectURL = async (file, forceDownload = false) => { + const expiresIn = getUrlExpiryTimeSpanWithFallback(classOptions.URLExpiryTimeSpan); return getSignedUrl( s3, new GetObjectCommand({ @@ -88,9 +89,7 @@ class AmazonS3Store extends UploadFS.Store { ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`, Bucket: classOptions.params.Bucket, }), - { - expiresIn: classOptions.URLExpiryTimeSpan, // seconds - }, + { expiresIn }, ); }; @@ -105,7 +104,7 @@ class AmazonS3Store extends UploadFS.Store { } file.AmazonS3 = { - path: classOptions.getPath(file), + path: classOptions.getPath(file as Omit), }; file.store = this.options.name; // assign store to file @@ -204,7 +203,7 @@ class AmazonS3Store extends UploadFS.Store { }; this.getUrlExpiryTimeSpan = async () => { - return classOptions.URLExpiryTimeSpan || null; + return getUrlExpiryTimeSpanWithFallback(classOptions.URLExpiryTimeSpan); }; } } diff --git a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts b/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts index e2b71ac8052d7..03b74df609b01 100644 --- a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts +++ b/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts @@ -3,11 +3,11 @@ import { Storage } from '@google-cloud/storage'; import type { IUpload } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { check } from 'meteor/check'; -import type { OptionalId } from 'mongodb'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { UploadFS } from '../../../../server/ufs'; import type { StoreOptions } from '../../../../server/ufs/ufs-store'; +import { getUrlExpiryTimeSpanWithFallback } from '../../server/lib/urlExpiry'; type GStoreOptions = StoreOptions & { connection: { @@ -19,11 +19,11 @@ type GStoreOptions = StoreOptions & { }; bucket: string; URLExpiryTimeSpan: number; - getPath: (file: OptionalId) => string; + getPath: (file: Omit) => string; }; class GoogleStorageStore extends UploadFS.Store { - protected getPath: (file: IUpload) => string; + protected getPath: (file: Omit) => string; constructor(options: GStoreOptions) { super(options); @@ -56,16 +56,22 @@ class GoogleStorageStore extends UploadFS.Store { }; this.getRedirectURL = async (file, forceDownload = false) => { + const expirySeconds = getUrlExpiryTimeSpanWithFallback(options.URLExpiryTimeSpan); const params: GetSignedUrlConfig = { action: 'read', responseDisposition: forceDownload ? 'attachment' : 'inline', - expires: Date.now() + options.URLExpiryTimeSpan * 1000, + version: 'v2', + expires: Date.now() + expirySeconds * 1000, }; const res = await bucket.file(this.getPath(file)).getSignedUrl(params); return res[0]; }; + this.getUrlExpiryTimeSpan = async () => { + return getUrlExpiryTimeSpanWithFallback(options.URLExpiryTimeSpan); + }; + /** * Creates the file in the collection * @param file @@ -80,7 +86,7 @@ class GoogleStorageStore extends UploadFS.Store { } file.GoogleStorage = { - path: options.getPath(file), + path: options.getPath(file as Omit), }; file.store = this.options.name; // assign store to file diff --git a/apps/meteor/app/file-upload/ufs/Webdav/server.ts b/apps/meteor/app/file-upload/ufs/Webdav/server.ts index e5a8a62b5d059..7eccb59d609cf 100644 --- a/apps/meteor/app/file-upload/ufs/Webdav/server.ts +++ b/apps/meteor/app/file-upload/ufs/Webdav/server.ts @@ -1,9 +1,8 @@ -import stream from 'stream'; +import stream from 'node:stream'; import type { IUpload } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { check } from 'meteor/check'; -import type { OptionalId } from 'mongodb'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { UploadFS } from '../../../../server/ufs'; @@ -19,11 +18,11 @@ type WebdavOptions = StoreOptions & { }; }; uploadFolderPath: string; - getPath: (file: OptionalId) => string; + getPath: (file: Omit) => string; }; class WebdavStore extends UploadFS.Store { - protected getPath: (file: IUpload) => string; + protected getPath: (file: Omit) => string; constructor(options: WebdavOptions) { super(options); @@ -72,7 +71,7 @@ class WebdavStore extends UploadFS.Store { } file.Webdav = { - path: options.getPath(file), + path: options.getPath(file as Omit), }; file.store = this.options.name; diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index 629684391bcbd..02a5a5b03edf3 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -1,8 +1,8 @@ -import type { ReadStream } from 'fs'; -import fs from 'fs'; -import fsp from 'fs/promises'; -import path from 'path'; -import { Readable } from 'stream'; +import type { ReadStream } from 'node:fs'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import { Readable } from 'node:stream'; import type { ObjectId } from 'bson'; import { MongoInternals } from 'meteor/mongo'; diff --git a/apps/meteor/app/file/server/functions/sanitizeFileName.ts b/apps/meteor/app/file/server/functions/sanitizeFileName.ts index 5b9ca233cbe07..fb19617afd496 100644 --- a/apps/meteor/app/file/server/functions/sanitizeFileName.ts +++ b/apps/meteor/app/file/server/functions/sanitizeFileName.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; export function sanitizeFileName(fileName: string) { const base = path.basename(fileName); diff --git a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts index 56aa0293aedb0..35b88c4267138 100644 --- a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts +++ b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts @@ -1,5 +1,5 @@ -import http from 'http'; -import https from 'https'; +import http from 'node:http'; +import https from 'node:https'; import { api } from '@rocket.chat/core-services'; import type { IImport, MessageAttachment, IUpload, IImporterShortSelection } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index 581172a617b96..dcf2d772bd6f1 100644 --- a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'node:fs'; import type { IImport, IImportUser } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; diff --git a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts index 3ae1df007f75a..56b49df61cd44 100644 --- a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts @@ -158,6 +158,7 @@ export class RecordConverter({ - async sendMessage(message) { - const uid = getUserId(); - if (!uid || trim(message.msg) === '') { - return false; - } - const messageAlreadyExists = message._id && Messages.state.get(message._id); - if (messageAlreadyExists) { - return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); - } - const user = getUser(); - if (!user?.username) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendMessage' }); - } - message.ts = new Date(); - message.u = { +export const runOptimisticSendMessage = async ( + message: Partial & { rid: IMessage['rid']; msg: IMessage['msg'] }, +): Promise => { + const uid = getUserId(); + if (!uid || trim(message.msg) === '') { + return; + } + const messageAlreadyExists = message._id && Messages.state.get(message._id); + if (messageAlreadyExists) { + dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); + return; + } + const user = getUser(); + if (!user?.username) { + return; + } + + const room = Rooms.state.get(message.rid); + if (room?.federated) { + return; + } + + const optimistic: IMessage = { + ...(message as IMessage), + ts: new Date(), + u: { _id: uid, username: user.username, name: user.name || '', - }; - message.temp = true; - if (settings.peek('Message_Read_Receipt_Enabled')) { - message.unread = true; - } + }, + temp: true, + ...(settings.peek('Message_Read_Receipt_Enabled') ? { unread: true } : {}), + }; - // If the room is federated, send the message to matrix only - const room = Rooms.state.get(message.rid); - if (room?.federated) { - return; - } + const processed = await onClientMessageReceived(optimistic); + Messages.state.store(processed); - await onClientMessageReceived(message as IMessage).then((message) => { - Messages.state.store(message); - return clientCallbacks.run('afterSaveMessage', message, { room, user }); - }); - }, -}); + if (processed.tmid) { + upsertThreadMessageInCache(processed, processed.rid, processed.tmid); + } +}; diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index 6c6e11a50a4c4..702622053aeb3 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -78,24 +78,29 @@ const getFields = (canViewAllInfo: boolean): Record => ({ const findTargetUser = (type: string, value: string, opts: any) => { if (type === 'importId') return Users.findOneByImportId(value, opts); if (type === 'email') return Users.findOneByEmailAddress(value, opts); + if (type === 'freeSwitchExtension') return Users.findOneByFreeSwitchExtension(value, opts); return Users.findOneByIdOrUsername(value, opts); }; -export async function getFullUserDataByIdOrUsernameOrImportIdOrEmail( +export async function getFullUserDataByUniqueSearchTerm( userId: string, searchValue: string, - searchType: 'id' | 'username' | 'importId' | 'email', + searchType: 'id' | 'username' | 'importId' | 'email' | 'freeSwitchExtension', ): Promise { - const caller = await Users.findOneById(userId, { projection: { username: 1, importIds: 1, emails: 1 } }); + const caller = await Users.findOneById(userId, { projection: { username: 1, importIds: 1, emails: 1, freeSwitchExtension: 1 } }); + if (!caller) { return null; } + const myself = (searchType === 'id' && searchValue === userId) || (searchType === 'username' && searchValue === caller.username) || (searchType === 'importId' && caller.importIds?.includes(searchValue)) || + (searchType === 'freeSwitchExtension' && caller.freeSwitchExtension === searchValue) || (searchType === 'email' && caller.emails?.some((email: IUserEmail) => email.address.trim().toLowerCase() === searchValue.trim().toLowerCase())); + const canViewAllInfo = !!myself || (await hasPermissionAsync(userId, 'view-full-other-user-info')); // Only search for importId/email if the user has permission to view them diff --git a/apps/meteor/app/lib/server/functions/notifications/desktop.ts b/apps/meteor/app/lib/server/functions/notifications/desktop.ts index 9b49f53582063..4554656d408b6 100644 --- a/apps/meteor/app/lib/server/functions/notifications/desktop.ts +++ b/apps/meteor/app/lib/server/functions/notifications/desktop.ts @@ -67,6 +67,7 @@ export async function notifyDesktopUser({ }; metrics.notificationsSent.inc({ notification_type: 'desktop' }); + metrics.notificationsSentTotal.inc({ notification_type: 'desktop' }); void api.broadcast('notify.desktop', userId, payload); } diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 7bdbd0d6e990a..4de543abb7dd6 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -178,13 +178,14 @@ export async function getEmailData({ message, receiver, sender, subscription, ro )}${message.tmid || message._id}@${replyto.split('@')[1]}`; } - metrics.notificationsSent.inc({ notification_type: 'email' }); return email; } export function sendEmailFromData(data) { - metrics.notificationsSent.inc({ notification_type: 'email' }); - return Mailer.send(data); + return Mailer.send(data).then(() => { + metrics.notificationsSent.inc({ notification_type: 'email' }); + metrics.notificationsSentTotal.inc({ notification_type: 'email' }); + }); } export function shouldNotifyEmail({ diff --git a/apps/meteor/app/lib/server/functions/saveUser/setPasswordUpdater.ts b/apps/meteor/app/lib/server/functions/saveUser/setPasswordUpdater.ts index f9218d64449b3..7f51b451b9002 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/setPasswordUpdater.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/setPasswordUpdater.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { IUser } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/model-typings'; diff --git a/apps/meteor/app/lib/server/lib/debug.js b/apps/meteor/app/lib/server/lib/debug.js index 71daf1a4fbcfc..d4c129a0767ff 100644 --- a/apps/meteor/app/lib/server/lib/debug.js +++ b/apps/meteor/app/lib/server/lib/debug.js @@ -57,11 +57,14 @@ const wrapMethods = function (name, originalHandler, methodsMap) { const method = name === 'stream' ? `${name}:${originalArgs[0]}` : name; - const end = metrics.meteorMethods.startTimer({ + const meteorMethodLabels = { method, has_connection: this.connection != null, has_user: this.userId != null, - }); + }; + + const end = metrics.meteorMethods.startTimer(meteorMethodLabels); + const endHistogram = metrics.meteorMethodsSeconds.startTimer(meteorMethodLabels); logger.method({ method, @@ -84,6 +87,7 @@ const wrapMethods = function (name, originalHandler, methodsMap) { async () => { const result = await originalHandler.apply(this, originalArgs); end(); + endHistogram(); return result; }, ); @@ -115,10 +119,12 @@ Meteor.publish = function (name, func) { }); const end = metrics.meteorSubscriptions.startTimer({ subscription: name }); + const endHistogram = metrics.meteorSubscriptionsSeconds.startTimer({ subscription: name }); const originalReady = this.ready; this.ready = function () { end(); + endHistogram(); return originalReady.apply(this, args); }; diff --git a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts index ee74d472ff05c..0d1bff3250a4c 100644 --- a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts +++ b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts @@ -40,6 +40,7 @@ export const apiDeprecationLogger = ((logger) => { writeDeprecationHeader(res, 'endpoint-deprecation', message, version); metrics.deprecations.inc({ type: 'deprecation', kind: 'endpoint', name: endpoint }); + metrics.deprecationsTotal.inc({ type: 'deprecation', kind: 'endpoint', name: endpoint }); logger.warn({ msg: message, endpoint, version, info }); }, @@ -63,6 +64,7 @@ export const apiDeprecationLogger = ((logger) => { } metrics.deprecations.inc({ type: 'parameter-deprecation', kind: 'endpoint', name: endpoint, params: parameter }); + metrics.deprecationsTotal.inc({ type: 'parameter-deprecation', kind: 'endpoint', name: endpoint, params: parameter }); writeDeprecationHeader(res, 'parameter-deprecation', message, version); @@ -92,6 +94,7 @@ export const apiDeprecationLogger = ((logger) => { compareVersions(version, message); metrics.deprecations.inc({ type: 'invalid-usage', kind: 'endpoint', name: endpoint, params: parameter }); + metrics.deprecationsTotal.inc({ type: 'invalid-usage', kind: 'endpoint', name: endpoint, params: parameter }); writeDeprecationHeader(res, 'invalid-usage', message, version); @@ -100,20 +103,23 @@ export const apiDeprecationLogger = ((logger) => { }; })(deprecationLogger.section('API')); +type DeprecationPath = T extends `/${string}` ? (T extends PathPattern ? T : never) : string; + +type DeprecationInfo = DeprecationPath | Array>; + export const methodDeprecationLogger = ((logger) => { return { - method: ( - method: string, - version: DeprecationLoggerNextPlannedVersion, - info: T extends `/${string}` ? (T extends PathPattern ? T : never) : string, - ) => { - const replacement = typeof info === 'string' ? info : `Use the ${info} endpoint instead`; + method: (method: string, version: DeprecationLoggerNextPlannedVersion, info: DeprecationInfo) => { + const infoArray = Array.isArray(info) ? info : [info]; + const paths = infoArray.map((p) => (typeof p === 'string' && p.startsWith('/') ? `"${p}"` : p)).join(' or '); + const replacement = infoArray.length > 0 ? `Use the ${paths} endpoint${infoArray.length > 1 ? 's' : ''} instead` : ''; const message = `The method "${method}" is deprecated and will be removed on version ${version}${replacement ? ` (${replacement})` : ''}`; if (process.env.TEST_MODE === 'true') { throw new Error(message); } compareVersions(version, message); metrics.deprecations.inc({ type: 'deprecation', name: method, kind: 'method' }); + metrics.deprecationsTotal.inc({ type: 'deprecation', name: method, kind: 'method' }); logger.warn({ msg: message, method, version, replacement }); }, parameter: (method: string, parameter: string, version: DeprecationLoggerNextPlannedVersion) => { @@ -123,6 +129,7 @@ export const methodDeprecationLogger = ((logger) => { } metrics.deprecations.inc({ type: 'parameter-deprecation', name: method, params: parameter }); + metrics.deprecationsTotal.inc({ type: 'parameter-deprecation', name: method, params: parameter }); compareVersions(version, message); logger.warn({ msg: message, method, parameter, version }); @@ -149,6 +156,7 @@ export const methodDeprecationLogger = ((logger) => { compareVersions(version, message); metrics.deprecations.inc({ type: 'invalid-usage', name: method, params: parameter, kind: 'method' }); + metrics.deprecationsTotal.inc({ type: 'invalid-usage', name: method, params: parameter, kind: 'method' }); logger.warn({ msg: message, method, parameter, version }); }, diff --git a/apps/meteor/app/lib/server/lib/processDirectEmail.ts b/apps/meteor/app/lib/server/lib/processDirectEmail.ts index e88b7d58468e4..e930add5716e8 100644 --- a/apps/meteor/app/lib/server/lib/processDirectEmail.ts +++ b/apps/meteor/app/lib/server/lib/processDirectEmail.ts @@ -109,8 +109,6 @@ export const processDirectEmail = async function (email: ParsedMail): Promise = { ts: tsDiff < 10000 ? ts : new Date(), msg, @@ -120,5 +118,10 @@ export const processDirectEmail = async function (email: ParsedMail): Promise async (reply, input) => { if (reply.allowed === false) { rateLimiterLog({ msg, reply, input }); - metrics.ddpRateLimitExceeded.inc({ + const rateLimitLabels = { limit_name: name, user_id: input.userId, client_address: input.clientAddress, type: input.type, name: input.name, connection_id: input.connectionId, - }); + }; + metrics.ddpRateLimitExceeded.inc(rateLimitLabels); + metrics.ddpRateLimitExceededTotal.inc(rateLimitLabels); // sleep before sending the error to slow down next requests if (slowDownRate > 0 && reply.numInvocationsExceeded) { await sleep(slowDownRate * reply.numInvocationsExceeded); diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 5fc9be444ec98..ed48999575202 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -5,7 +5,6 @@ import { type IOmnichannelAgent, type Serialized, } from '@rocket.chat/core-typings'; -import { Tracker } from 'meteor/tracker'; import { useLivechatInquiryStore } from '../../../../../client/hooks/useLivechatInquiryStore'; import { queryClient } from '../../../../../client/lib/queryClient'; @@ -64,8 +63,10 @@ const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { return queryClient.invalidateQueries({ queryKey: ['rooms', { reference: inquiry.rid, type: 'l' }] }); }; +const INQUIRY_COUNT_SETTING = 'Livechat_guest_pool_max_number_incoming_livechats_displayed'; + const getInquiriesFromAPI = async () => { - const count = settings.peek('Livechat_guest_pool_max_number_incoming_livechats_displayed') ?? 0; + const count = settings.peek(INQUIRY_COUNT_SETTING) ?? 0; const { inquiries } = await sdk.rest.get('/v1/livechat/inquiries.queuedForUser', { count }); return inquiries; }; @@ -140,10 +141,12 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { const cleanDepartmentListeners = addListenerForeachDepartment(agentDepartments); const globalCleanup = addGlobalListener(); - const computation = Tracker.autorun(async () => { - const inquiriesFromAPI = await getInquiriesFromAPI(); + const refetchInquiries = async () => updateInquiries(await getInquiriesFromAPI()); + + await refetchInquiries(); - await updateInquiries(inquiriesFromAPI); + const unobserveInquiryCount = settings.observe(INQUIRY_COUNT_SETTING, () => { + void refetchInquiries(); }); return () => { @@ -152,8 +155,8 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { cleanAgentListener?.(); cleanDepartmentListeners?.(); globalCleanup?.(); + unobserveInquiryCount(); departments.clear(); - computation.stop(); }; } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 533409d342ac2..31a8265a4ec07 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -26,7 +26,7 @@ import { createRoom } from '../../../server/lib/rooms'; const logger = new Logger('SMS'); -const getUploadFile = async (details: Omit, fileUrl: string) => { +const getUploadFile = async (details: Omit, fileUrl: string) => { const response = await fetch(fileUrl, { ignoreSsrfValidation: false, allowList: settings.get('SSRF_Allowlist'), diff --git a/apps/meteor/app/livechat/server/business-hour/LivechatBusinessHours.ts b/apps/meteor/app/livechat/server/business-hour/LivechatBusinessHours.ts index 92f3ed840682f..188b427874aef 100644 --- a/apps/meteor/app/livechat/server/business-hour/LivechatBusinessHours.ts +++ b/apps/meteor/app/livechat/server/business-hour/LivechatBusinessHours.ts @@ -3,7 +3,7 @@ import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; -export const createDefaultBusinessHourRow = (): ILivechatBusinessHour => { +export const createDefaultBusinessHourRow = (): Omit => { const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; const closedDays = ['Saturday', 'Sunday']; return { diff --git a/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts index fb379eb9b6693..aa70b15175c61 100644 --- a/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts +++ b/apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts @@ -2,7 +2,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import moment from 'moment'; export const filterBusinessHoursThatMustBeOpened = async ( - businessHours: ILivechatBusinessHour[], + businessHours: Omit[], ): Promise[]> => { const currentTime = moment(moment().format('dddd:HH:mm:ss'), 'dddd:HH:mm:ss'); diff --git a/apps/meteor/app/livechat/server/lib/messages.ts b/apps/meteor/app/livechat/server/lib/messages.ts index e04a3f4ddcd72..558aecde98f5c 100644 --- a/apps/meteor/app/livechat/server/lib/messages.ts +++ b/apps/meteor/app/livechat/server/lib/messages.ts @@ -1,5 +1,5 @@ -import dns from 'dns'; -import * as util from 'util'; +import dns from 'node:dns'; +import * as util from 'node:util'; import type { ILivechatVisitor, AtLeast, IMessage, IUser, IOmnichannelRoomInfo, SelectedAgent } from '@rocket.chat/core-typings'; import { LivechatDepartment, Messages } from '@rocket.chat/models'; diff --git a/apps/meteor/app/livechat/server/lib/webhooks.ts b/apps/meteor/app/livechat/server/lib/webhooks.ts index 661b428cc7cb8..de49a6bb0cae7 100644 --- a/apps/meteor/app/livechat/server/lib/webhooks.ts +++ b/apps/meteor/app/livechat/server/lib/webhooks.ts @@ -34,6 +34,7 @@ export async function sendRequest( if (result.status === 200) { metrics.totalLivechatWebhooksSuccess.inc(); + metrics.totalLivechatWebhooksSuccessTotal.inc(); await cb?.(result); return result; } @@ -46,10 +47,12 @@ export async function sendRequest( response: await result.text(), }); metrics.totalLivechatWebhooksFailures.inc(); + metrics.totalLivechatWebhooksFailuresTotal.inc(); return; } metrics.totalLivechatWebhooksFailures.inc(); + metrics.totalLivechatWebhooksFailuresTotal.inc(); throw new Error(await result.text()); } catch (err) { const retryAfter = timeout * 4; diff --git a/apps/meteor/app/livechat/server/livechat.ts b/apps/meteor/app/livechat/server/livechat.ts index 8928ee826eaf9..3efc2feb17bd3 100644 --- a/apps/meteor/app/livechat/server/livechat.ts +++ b/apps/meteor/app/livechat/server/livechat.ts @@ -1,4 +1,4 @@ -import url from 'url'; +import url from 'node:url'; import jsdom from 'jsdom'; import mem from 'mem'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 7dd58f35bed3e..61e4bd7421b88 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -1,4 +1,4 @@ -import type { ServerResponse } from 'http'; +import type { ServerResponse } from 'node:http'; import type { IUser, IIncomingMessage, IPersonalAccessToken, IRole } from '@rocket.chat/core-typings'; import { CredentialTokens, Rooms, Users, Roles } from '@rocket.chat/models'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts index 824b7ec684dcc..edcea9f72794e 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts @@ -1,7 +1,7 @@ -import crypto from 'crypto'; -import querystring from 'querystring'; -import util from 'util'; -import zlib from 'zlib'; +import crypto from 'node:crypto'; +import querystring from 'node:querystring'; +import util from 'node:util'; +import zlib from 'node:zlib'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index 8d99fa18b70ae..c7bd6d032fb65 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -1,5 +1,5 @@ -import { EventEmitter } from 'events'; -import zlib from 'zlib'; +import { EventEmitter } from 'node:events'; +import zlib from 'node:zlib'; import type { Logger } from '@rocket.chat/logger'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/signature/validateRedirectSignature.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/signature/validateRedirectSignature.ts index 9dd73052b8214..31c57ceb9bf9e 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/signature/validateRedirectSignature.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/signature/validateRedirectSignature.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { SAMLRedirectEnvelope } from '../../definition/SAMLEnvelope'; import { SAMLUtils } from '../Utils'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/listener.ts b/apps/meteor/app/meteor-accounts-saml/server/listener.ts index 8747bd59a8714..665be0506531f 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/listener.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/listener.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage, ServerResponse } from 'http'; +import type { IncomingMessage, ServerResponse } from 'node:http'; import bodyParser from 'body-parser'; import express from 'express'; diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 6a393bb406506..079e4d1be3692 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -1,4 +1,4 @@ -import http from 'http'; +import http from 'node:http'; import { Statistics } from '@rocket.chat/models'; import { tracerSpan } from '@rocket.chat/tracing'; @@ -92,6 +92,7 @@ app.use('/metrics', (_req, res) => { .metrics() .then((data) => { metrics.metricsRequests.inc(); + metrics.metricsRequestsTotal.inc(); metrics.metricsSize.set(data.length); res.end(data); diff --git a/apps/meteor/app/metrics/server/lib/metrics.ts b/apps/meteor/app/metrics/server/lib/metrics.ts index c35cd51f70ca6..939b2edbd0089 100644 --- a/apps/meteor/app/metrics/server/lib/metrics.ts +++ b/apps/meteor/app/metrics/server/lib/metrics.ts @@ -1,18 +1,33 @@ +import { performance } from 'node:perf_hooks'; + import client from 'prom-client'; const percentiles = [0.01, 0.1, 0.5, 0.9, 0.95, 0.99, 1]; +const latencyBuckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0]; +const queueWaitBuckets = [1, 5, 10, 30, 60, 120, 300, 600, 900, 1800, 3600]; + export const metrics = { deprecations: new client.Counter({ name: 'rocketchat_deprecations', labelNames: ['type', 'kind', 'name', 'params'], help: 'cumulated number of deprecations being used', }), + deprecationsTotal: new client.Counter({ + name: 'rocketchat_deprecations_total', + labelNames: ['type', 'kind', 'name', 'params'], + help: 'cumulated number of deprecations being used', + }), metricsRequests: new client.Counter({ name: 'rocketchat_metrics_requests', labelNames: ['notification_type'], help: 'cumulated number of calls to the metrics endpoint', }), + metricsRequestsTotal: new client.Counter({ + name: 'rocketchat_metrics_requests_total', + labelNames: ['notification_type'], + help: 'cumulated number of calls to the metrics endpoint', + }), metricsSize: new client.Gauge({ name: 'rocketchat_metrics_size', help: 'size of the metrics response in chars', @@ -28,24 +43,59 @@ export const metrics = { labelNames: ['method', 'has_connection', 'has_user'], percentiles, }), + meteorMethodsSeconds: new client.Histogram({ + name: 'rocketchat_meteor_methods_seconds', + help: 'histogram of meteor methods count and time in seconds', + labelNames: ['method', 'has_connection', 'has_user'], + buckets: latencyBuckets, + }), rocketchatCallbacks: new client.Summary({ name: 'rocketchat_callbacks', help: 'summary of rocketchat callbacks count and time', labelNames: ['hook', 'callback'], percentiles, }), + rocketchatCallbacksSeconds: new client.Histogram({ + name: 'rocketchat_callbacks_seconds', + help: 'histogram of rocketchat callbacks count and time in seconds', + labelNames: ['hook', 'callback'], + buckets: latencyBuckets, + }), rocketchatHooks: new client.Summary({ name: 'rocketchat_hooks', help: 'summary of rocketchat hooks count and time', labelNames: ['hook', 'callbacks_length'], percentiles, }), + rocketchatHooksSeconds: new client.Histogram({ + name: 'rocketchat_hooks_seconds', + help: 'histogram of rocketchat hooks count and time in seconds', + labelNames: ['hook', 'callbacks_length'], + buckets: latencyBuckets, + }), rocketchatRestApi: new client.Summary({ name: 'rocketchat_rest_api', help: 'summary of rocketchat rest api count and time', labelNames: ['method', 'entrypoint', 'user_agent', 'status', 'version'], percentiles, }), + rocketchatRestApiSeconds: new client.Histogram({ + name: 'rocketchat_rest_api_seconds', + help: 'histogram of rocketchat rest api count and time in seconds', + labelNames: ['method', 'entrypoint', 'status', 'version'], + buckets: latencyBuckets, + }), + rocketchatRestApiResponseSizeBytes: new client.Histogram({ + name: 'rocketchat_rest_api_response_size_bytes', + help: 'histogram of rocketchat rest api response sizes in bytes', + labelNames: ['method', 'entrypoint', 'status', 'version'], + buckets: [0, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000], + }), + rocketchatRestApiActiveRequests: new client.Gauge({ + name: 'rocketchat_rest_api_active_requests', + help: 'number of currently active rest api requests', + labelNames: ['method'], + }), meteorSubscriptions: new client.Summary({ name: 'rocketchat_meteor_subscriptions', @@ -53,16 +103,31 @@ export const metrics = { labelNames: ['subscription'], percentiles, }), + meteorSubscriptionsSeconds: new client.Histogram({ + name: 'rocketchat_meteor_subscriptions_seconds', + help: 'histogram of meteor subscriptions count and time in seconds', + labelNames: ['subscription'], + buckets: latencyBuckets, + }), messagesSent: new client.Counter({ name: 'rocketchat_message_sent', help: 'cumulated number of messages sent', }), + messagesSentTotal: new client.Counter({ + name: 'rocketchat_messages_sent_total', + help: 'cumulated number of messages sent', + }), notificationsSent: new client.Counter({ name: 'rocketchat_notification_sent', labelNames: ['notification_type'], help: 'cumulated number of notifications sent', }), + notificationsSentTotal: new client.Counter({ + name: 'rocketchat_notifications_sent_total', + labelNames: ['notification_type'], + help: 'cumulated number of notifications sent', + }), messageRoundtripTime: new client.Summary({ name: 'rocketchat_messages_roundtrip_time_summary', help: 'time spent by a message from save to receive back', @@ -71,6 +136,11 @@ export const metrics = { ageBuckets: 5, // pruneAgedBuckets: true, // Type not added to prom-client on 14.2 https://github.com/siimon/prom-client/pull/558 }), + messageRoundtripTimeSeconds: new client.Histogram({ + name: 'rocketchat_messages_roundtrip_time_seconds', + help: 'time in seconds spent by a message from save to receive back', + buckets: latencyBuckets, + }), ddpSessions: new client.Gauge({ name: 'rocketchat_ddp_sessions_count', @@ -89,6 +159,11 @@ export const metrics = { labelNames: ['limit_name', 'user_id', 'client_address', 'type', 'name', 'connection_id'], help: 'number of times a ddp rate limiter was exceeded', }), + ddpRateLimitExceededTotal: new client.Counter({ + name: 'rocketchat_ddp_rate_limit_exceeded_total', + labelNames: ['limit_name', 'user_id', 'client_address', 'type', 'name', 'connection_id'], + help: 'number of times a ddp rate limiter was exceeded', + }), version: new client.Gauge({ name: 'rocketchat_version', @@ -189,6 +264,12 @@ export const metrics = { labelNames: ['bridge', 'method', 'app_id'], percentiles, }), + appBridgeMethodsSeconds: new client.Histogram({ + name: 'rocketchat_apps_bridge_methods_seconds', + help: 'histogram of app bridge method calls count and time in seconds', + labelNames: ['bridge', 'method', 'app_id'], + buckets: latencyBuckets, + }), // Meteor Facts meteorFacts: new client.Gauge({ @@ -204,10 +285,18 @@ export const metrics = { name: 'rocketchat_livechat_webhooks_success', help: 'successful livechat webhooks', }), + totalLivechatWebhooksSuccessTotal: new client.Counter({ + name: 'rocketchat_livechat_webhooks_success_total', + help: 'successful livechat webhooks', + }), totalLivechatWebhooksFailures: new client.Counter({ name: 'rocketchat_livechat_webhooks_failures', help: 'failed livechat webhooks', }), + totalLivechatWebhooksFailuresTotal: new client.Counter({ + name: 'rocketchat_livechat_webhooks_failures_total', + help: 'failed livechat webhooks', + }), totalItemsProcessedByQueue: new client.Counter({ name: 'rocketchat_queue_items_processed_total', @@ -230,6 +319,22 @@ export const metrics = { help: 'Time taken in seconds for an item to be processed for the first time by Omni queues', percentiles, }), + timeToQueueProcessingByQueueHistogram: new client.Histogram({ + name: 'rocketchat_queue_wait_duration_seconds', + labelNames: ['queue'], + help: 'Histogram of time taken in seconds for an item to be processed for the first time by Omni queues', + buckets: queueWaitBuckets, + }), }; // Metrics +let eluBase = performance.eventLoopUtilization(); + +new client.Gauge({ + name: 'nodejs_event_loop_utilization_ratio', + help: 'Event Loop Utilization (ELU) as reported by NodeJS', + collect() { + this.set(performance.eventLoopUtilization(eluBase).utilization); + eluBase = performance.eventLoopUtilization(); + }, +}); diff --git a/apps/meteor/app/notifications/client/index.ts b/apps/meteor/app/notifications/client/index.ts deleted file mode 100644 index 47c2de4137407..0000000000000 --- a/apps/meteor/app/notifications/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib/Presence'; diff --git a/apps/meteor/app/notifications/client/lib/Presence.ts b/apps/meteor/app/notifications/client/lib/Presence.ts deleted file mode 100644 index d6890d12aa52d..0000000000000 --- a/apps/meteor/app/notifications/client/lib/Presence.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UserStatus } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { Presence } from '../../../../client/lib/presence'; -import { streamerCentral } from '../../../../client/lib/streamer'; - -// TODO implement API on Streamer to be able to listen to all streamed data -// this is a hacky way to listen to all streamed data from user-presence Streamer - -streamerCentral.getStreamer('user-presence', { ddpConnection: Meteor.connection }); - -type args = [username: string, statusChanged?: UserStatus, statusText?: string]; - -export const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY, UserStatus.DISABLED]; - -streamerCentral.on('stream-user-presence', (uid: string, [username, statusChanged, statusText]: args) => { - Presence.notify({ _id: uid, username, status: STATUS_MAP[statusChanged as any], statusText }); -}); diff --git a/apps/meteor/app/notifications/server/lib/Presence.ts b/apps/meteor/app/notifications/server/lib/Presence.ts index bfb327d8eff1d..fa8a2ac9630ab 100644 --- a/apps/meteor/app/notifications/server/lib/Presence.ts +++ b/apps/meteor/app/notifications/server/lib/Presence.ts @@ -2,6 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { StreamerEvents } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; +import { Streamer } from '../../../../server/modules/streamer/streamer.module'; import type { IPublication, IStreamerConstructor, Connection, IStreamer } from '../../../../server/modules/streamer/types'; type UserPresenceStreamProps = { @@ -48,7 +49,17 @@ class UserPresence { run = (args: UserPresenceStreamArgs): void => { const payload = this.streamer.changedPayload(this.streamer.subscriptionName, args.uid, { ...args, eventName: args.uid }); // there is no good explanation to keep eventName, I just want to save one 'DDPCommon.parseDDP' on the client side, so I'm trying to fit the Meteor Streamer's payload - if (payload) this.publication._session.socket?.send(payload); + if (!payload) { + return; + } + // after meteor 3.4.1 immediately after a disconnection session becomes null (which is not wrong) + // we were just not counting on this, session is _session so we actually should not use it + // now after any await, the session can potentially be null, so we need to check for that + if (!Streamer.isPublicationActive(this.publication)) { + return; + } + + this.publication._session.socket.send(payload); }; stop(): void { diff --git a/apps/meteor/app/push-notifications/server/lib/PushNotification.ts b/apps/meteor/app/push-notifications/server/lib/PushNotification.ts index 7fd7f404ac970..33c752825ae97 100644 --- a/apps/meteor/app/push-notifications/server/lib/PushNotification.ts +++ b/apps/meteor/app/push-notifications/server/lib/PushNotification.ts @@ -103,8 +103,10 @@ class PushNotification { idOnly, }); - metrics.notificationsSent.inc({ notification_type: 'mobile' }); await Push.send(config); + + metrics.notificationsSent.inc({ notification_type: 'mobile' }); + metrics.notificationsSentTotal.inc({ notification_type: 'mobile' }); } async getNotificationForMessageId({ diff --git a/apps/meteor/app/reactions/client/index.ts b/apps/meteor/app/reactions/client/index.ts deleted file mode 100644 index 935ab48e93ad6..0000000000000 --- a/apps/meteor/app/reactions/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './methods/setReaction'; diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index 96cd4bcb95910..093fdd9ea2b0c 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -1,84 +1,81 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { getUser, getUserId } from '../../../../client/lib/user'; import { Rooms, Subscriptions, Messages } from '../../../../client/stores'; import { emoji } from '../../../emoji/client'; -Meteor.methods({ - async setReaction(reaction, messageId) { - if (!getUserId()) { - throw new Meteor.Error(203, 'User_logged_out'); - } - - const user = getUser(); +export const runOptimisticSetReaction = (reaction: string, messageId: IMessage['_id']): void => { + if (!getUserId()) { + return; + } - if (!user?.username) { - return false; - } + const user = getUser(); + if (!user?.username) { + return; + } - const message: IMessage | undefined = Messages.state.get(messageId); - if (!message) { - return false; - } + const message: IMessage | undefined = Messages.state.get(messageId); + if (!message) { + return; + } - const room = Rooms.state.get(message.rid); - if (!room) { - return false; - } - - if (message.private) { - return false; - } + const room = Rooms.state.get(message.rid); + if (!room) { + return; + } - if (!emoji.list[reaction]) { - return false; - } + if (message.private) { + return; + } - if (roomCoordinator.readOnly(room, user)) { - return false; - } + if (!emoji.list[reaction]) { + return; + } - if (!Subscriptions.state.find(({ rid }) => rid === message.rid)) { - return false; - } + if (roomCoordinator.readOnly(room, user)) { + return; + } - if (message.reactions?.[reaction] && message.reactions[reaction].usernames.indexOf(user.username) !== -1) { - message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(user.username), 1); + if (!Subscriptions.state.find(({ rid }) => rid === message.rid)) { + return; + } - if (message.reactions[reaction].usernames.length === 0) { - delete message.reactions[reaction]; - } + if (message.reactions?.[reaction] && message.reactions[reaction].usernames.indexOf(user.username) !== -1) { + message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(user.username), 1); - if (!message.reactions || typeof message.reactions !== 'object' || Object.keys(message.reactions).length === 0) { - delete message.reactions; - Messages.state.update( - (record) => record._id === messageId, - ({ reactions: _, ...record }) => record, - ); - } else { - Messages.state.update( - (record) => record._id === messageId, - (record) => ({ ...record, reactions: message.reactions }), - ); - } - } else { - if (!message.reactions) { - message.reactions = {}; - } - if (!message.reactions[reaction]) { - message.reactions[reaction] = { - usernames: [], - }; - } - message.reactions[reaction].usernames.push(user.username); + if (message.reactions[reaction].usernames.length === 0) { + delete message.reactions[reaction]; + } + if (!message.reactions || typeof message.reactions !== 'object' || Object.keys(message.reactions).length === 0) { + delete message.reactions; Messages.state.update( (record) => record._id === messageId, - (record) => ({ ...record, reactions: message.reactions }), + ({ reactions: _, ...record }) => record, ); + return; } - }, -}); + + Messages.state.update( + (record) => record._id === messageId, + (record) => ({ ...record, reactions: message.reactions }), + ); + return; + } + + if (!message.reactions) { + message.reactions = {}; + } + if (!message.reactions[reaction]) { + message.reactions[reaction] = { + usernames: [], + }; + } + message.reactions[reaction].usernames.push(user.username); + + Messages.state.update( + (record) => record._id === messageId, + (record) => ({ ...record, reactions: message.reactions }), + ); +}; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.ts b/apps/meteor/app/slackbridge/server/RocketAdapter.ts index cdf4cd8085af9..5c46d75368dda 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.ts +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ // @ts-nocheck -import util from 'util'; +import util from 'node:util'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.ts b/apps/meteor/app/slackbridge/server/SlackAdapter.ts index 831ca8a67cbce..b02ad32ea13a2 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.ts +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.ts @@ -4,9 +4,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ // @ts-nocheck -import http from 'http'; -import https from 'https'; -import url from 'url'; +import http from 'node:http'; +import https from 'node:https'; +import url from 'node:url'; import { Message } from '@rocket.chat/core-services'; import { Messages, Rooms, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/slashcommands-join/client/client.ts b/apps/meteor/app/slashcommands-join/client/client.ts index bc8d589f51ac4..878981a11951c 100644 --- a/apps/meteor/app/slashcommands-join/client/client.ts +++ b/apps/meteor/app/slashcommands-join/client/client.ts @@ -1,5 +1,3 @@ -import type { Meteor } from 'meteor/meteor'; - import { slashCommands } from '../../utils/client/slashCommand'; slashCommands.add({ @@ -10,7 +8,7 @@ slashCommands.add({ permission: 'view-c-room', }, result(err, _result: unknown, params: Record) { - if ((err as Meteor.Error).error === 'error-user-already-in-room') { + if ((err as { error?: string } | undefined)?.error === 'error-user-already-in-room') { params.cmd = 'open'; params.msg.msg = params.msg.msg.replace('join', 'open'); return void slashCommands.run({ command: 'open', params: params.params, message: params.msg, triggerId: '', userId: params.userId }); diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts index 90e05d1923563..1734d175c6c5d 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; +import { AppInstallationSource } from '@rocket.chat/apps/dist/server/storage/IAppStorageItem'; import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; -import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import mem from 'mem'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index c0aadb9965691..8c810acaad70b 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -1,5 +1,5 @@ import { log } from 'console'; -import os from 'os'; +import os from 'node:os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 7486bd9aacb07..289e1c53f00f5 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -56,8 +56,9 @@ padding-bottom: 24px; } - & ul.messages-list { - padding: 21px 0 10px; + /* Overlay scrollbars overrides styles, so we need an important here */ + & .messages-list { + padding: 21px 0 10px !important; } } diff --git a/apps/meteor/app/theme/server/server.ts b/apps/meteor/app/theme/server/server.ts index d0342df878973..34b71e38a303d 100644 --- a/apps/meteor/app/theme/server/server.ts +++ b/apps/meteor/app/theme/server/server.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/app/ui-master/server/index.ts b/apps/meteor/app/ui-master/server/index.ts index 53f47ca8ab4ef..24a673b3b9b85 100644 --- a/apps/meteor/app/ui-master/server/index.ts +++ b/apps/meteor/app/ui-master/server/index.ts @@ -6,6 +6,7 @@ import { Inject } from 'meteor/meteorhacks:inject-initial'; import { Tracker } from 'meteor/tracker'; import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; +import { getMessageMaxParseLength } from '../../../lib/getMessageMaxParseLength'; import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; import { settings } from '../../settings/server'; import { getURL } from '../../utils/server/getURL'; @@ -28,6 +29,10 @@ Meteor.startup(() => { injectIntoHead('noreferrer', ``); }); + settings.watch('Use_RC_SDK', (value) => { + injectIntoHead('Use_RC_SDK', ``); + }); + if (process.env.DISABLE_ANIMATION) { injectIntoHead( 'disable-animation', @@ -122,6 +127,9 @@ Meteor.startup(() => { })(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX); injectIntoHead('base', ``); + + const escapedMessageMaxParseLength = escapeHTML(String(getMessageMaxParseLength())); + injectIntoHead('MESSAGE_MAX_PARSE_LENGTH', ``); }); const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 47b63db4bb3f8..07423bc8ac5c7 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { NextHandleFunction } from 'connect'; import { Inject } from 'meteor/meteorhacks:inject-initial'; diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index b7e423b51d655..485a4636a8da8 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -33,22 +33,7 @@ window.addEventListener('Custom_Script_On_Logout', function() { ${settings.get('Custom_Script_On_Logout')} }) -${ - settings.get('Accounts_ForgetUserSessionOnWindowClose') - ? ` -window.addEventListener('load', function() { - if (window.localStorage) { - Object.keys(window.localStorage).forEach(function(key) { - window.sessionStorage.setItem(key, window.localStorage.getItem(key)); - }); - window.localStorage.clear(); - Meteor._localStorage = window.sessionStorage; - Accounts.config({ clientStorage: 'session' }); - } -}); -` - : '' -}`; +${settings.get('Accounts_ForgetUserSessionOnWindowClose') ? `window.Accounts_ForgetUserSessionOnWindowClose = true;` : ''}`; settings.watchMultiple( ['Custom_Script_Logged_Out', 'Custom_Script_Logged_In', 'Custom_Script_On_Logout', 'Accounts_ForgetUserSessionOnWindowClose'], diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 4922bc150d3e6..04447635ad45e 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -1,7 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import { Accounts } from 'meteor/accounts-base'; -import { Tracker } from 'meteor/tracker'; import type { RefObject } from 'react'; import { limitQuoteChain } from './limitQuoteChain'; @@ -9,11 +7,13 @@ import type { FormattingButton } from './messageBoxFormatting'; import { formattingButtons } from './messageBoxFormatting'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { createUploadsAPI } from '../../../../client/lib/chats/uploads'; +import { settings } from '../../../../client/lib/settings'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; export const createComposerAPI = ( input: HTMLTextAreaElement, - storageID: string, + persistDraft: (value: string) => void, + initialDraft: string, quoteChainLimit: number, composerRef: RefObject, { rid, tmid }: { rid: string; tmid?: string }, @@ -40,20 +40,13 @@ export const createComposerAPI = ( let _quotedMessages: IMessage[] = []; const persist = withDebouncing({ wait: 300 })(() => { - if (input.value) { - Accounts.storageLocation.setItem(storageID, input.value); - return; - } - - Accounts.storageLocation.removeItem(storageID); + persistDraft(input.value); }); const notifyQuotedMessagesUpdate = (): void => { emitter.emit('quotedMessagesUpdate'); }; - input.addEventListener('input', persist); - const setText = ( text: string, { @@ -202,26 +195,31 @@ export const createComposerAPI = ( setEditing(editing); }; - const [formatters, stopFormatterTracker] = (() => { + const [formatters, stopFormatterSubscription] = (() => { let actions: FormattingButton[] = []; - const c = Tracker.autorun(() => { + const recompute = (): void => { actions = formattingButtons.filter(({ condition }) => !condition || condition()); emitter.emit('formatting'); - }); + }; + recompute(); + // Coarse-grained: fires on every setting change, but the only condition() + // today is Katex_Enabled and the recompute is a cheap zustand read, so the + // extra work per unrelated setting change is negligible. + const stop = settings.observe('*', recompute); return [ { get: () => actions, subscribe: (callback: () => void) => emitter.on('formatting', callback), }, - c, + stop, ]; })(); const release = (): void => { input.removeEventListener('input', persist); - stopFormatterTracker.stop(); + stopFormatterSubscription(); }; const wrapSelection = (pattern: string): { selectionStart: number; selectionEnd: number; value: string } => { @@ -282,10 +280,12 @@ export const createComposerAPI = ( const insertNewLine = (): void => insertText('\n'); - setText(Accounts.storageLocation.getItem(storageID) ?? '', { + setText(initialDraft, { skipFocus: true, }); + input.addEventListener('input', persist); + // Gets the text that is connected to the cursor and replaces it with the given text const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => { const { selectionStart, selectionEnd } = input; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index af58cbe8590b4..6a3862e490dd0 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -101,6 +101,6 @@ export const formattingButtons: ReadonlyArray = [ } }, link: 'https://khan.github.io/KaTeX/function-support.html', - condition: () => settings.watch('Katex_Enabled') ?? true, + condition: () => settings.peek('Katex_Enabled') ?? true, }, ] as const; diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 14b1d291ef016..95f94a4e356c7 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -1,9 +1,7 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { differenceInMilliseconds } from 'date-fns'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import type { MutableRefObject } from 'react'; +import { useCallback, useSyncExternalStore } from 'react'; import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; import { getUserId } from '../../../../client/lib/user'; @@ -13,8 +11,6 @@ import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { Messages, Subscriptions } from '../../../../client/stores'; import { getUserPreference } from '../../../utils/client'; -const waitAfterFlush = () => new Promise((resolve) => Tracker.afterFlush(() => resolve(void 0))); - const processMessage = async (msg: IMessage & { ignored?: boolean }, { subscription }: { subscription?: ISubscription }) => { const userId = msg.u?._id; @@ -46,36 +42,37 @@ export async function upsertMessageBulk({ const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50; +export type RoomHistoryState = { + hasMore: boolean; + hasMoreNext: boolean; + isLoading: boolean; + unreadNotLoaded: number; + firstUnread: IMessage | undefined; + loaded: number | undefined; + oldestTs?: Date; + scroll?: { + scrollHeight: number; + scrollTop: number; + }; +}; + +const roomStateEvent = (rid: IRoom['_id']) => `state:${rid}` as const; + class RoomHistoryManagerClass extends Emitter { private lastRequest?: Date; - private histories: Record< - IRoom['_id'], - { - hasMore: ReactiveVar; - hasMoreNext: ReactiveVar; - isLoading: ReactiveVar; - unreadNotLoaded: ReactiveVar; - firstUnread: ReactiveVar; - loaded: number | undefined; - oldestTs?: Date; - scroll?: { - scrollHeight: number; - scrollTop: number; - }; - } - > = {}; + private histories: Record = {}; private requestsList: string[] = []; - public getRoom(rid: IRoom['_id']) { + public getRoom(rid: IRoom['_id']): RoomHistoryState { if (!this.histories[rid]) { this.histories[rid] = { - hasMore: new ReactiveVar(true), - hasMoreNext: new ReactiveVar(false), - isLoading: new ReactiveVar(false), - unreadNotLoaded: new ReactiveVar(0), - firstUnread: new ReactiveVar(undefined), + hasMore: true, + hasMoreNext: false, + isLoading: false, + unreadNotLoaded: 0, + firstUnread: undefined, loaded: undefined, }; } @@ -83,6 +80,16 @@ class RoomHistoryManagerClass extends Emitter { return this.histories[rid]; } + public updateRoom(rid: IRoom['_id'], patch: Partial): void { + const room = this.getRoom(rid); + Object.assign(room, patch); + this.emit(roomStateEvent(rid), room); + } + + public subscribeToRoom(rid: IRoom['_id'], cb: (state: RoomHistoryState) => void): () => void { + return this.on(roomStateEvent(rid), cb); + } + private async queue(): Promise { return new Promise((resolve) => { const requestId = crypto.randomUUID(); @@ -122,12 +129,12 @@ class RoomHistoryManagerClass extends Emitter { public async getMore(rid: IRoom['_id'], { limit = defaultLimit }: { limit?: number } = {}): Promise { const room = this.getRoom(rid); - if (Tracker.nonreactive(() => room.hasMore.get()) !== true) { + if (room.hasMore !== true) { return; } try { - room.isLoading.set(true); + this.updateRoom(rid, { isLoading: true }); await this.queue(); @@ -155,8 +162,10 @@ class RoomHistoryManagerClass extends Emitter { this.unqueue(); const { messages = [] } = result; - room.unreadNotLoaded.set(result.unreadNotLoaded); - room.firstUnread.set(result.firstUnread); + this.updateRoom(rid, { + unreadNotLoaded: result.unreadNotLoaded, + firstUnread: result.firstUnread, + }); if (messages.length > 0) { room.oldestTs = messages[messages.length - 1].ts; @@ -185,17 +194,16 @@ class RoomHistoryManagerClass extends Emitter { room.loaded += visibleMessages.length; if (messages.length < limit) { - room.hasMore.set(false); + this.updateRoom(rid, { hasMore: false }); } - if (room.hasMore.get() && (visibleMessages.length === 0 || room.loaded < limit)) { + if (room.hasMore && (visibleMessages.length === 0 || room.loaded < limit)) { return this.getMore(rid); } this.emit('loaded-messages'); } finally { - room.isLoading.set(false); - await waitAfterFlush(); + this.updateRoom(rid, { isLoading: false }); } } @@ -216,16 +224,15 @@ class RoomHistoryManagerClass extends Emitter { room.scroll = undefined; } - public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject) { + public async getMoreNext(rid: IRoom['_id']) { const room = this.getRoom(rid); - if (Tracker.nonreactive(() => room.hasMoreNext.get()) !== true) { + if (room.hasMoreNext !== true) { return; } await this.queue(); - atBottomRef.current = false; - room.isLoading.set(true); + this.updateRoom(rid, { isLoading: true }); const lastMessage = Messages.state.findFirst( (record) => record.rid === rid && record._hidden !== true, @@ -244,27 +251,25 @@ class RoomHistoryManagerClass extends Emitter { this.emit('loaded-messages'); - room.isLoading.set(false); + this.updateRoom(rid, { isLoading: false }); if (!room.loaded) { room.loaded = 0; } room.loaded += result.messages.length; if (result.messages.length < defaultLimit) { - room.hasMoreNext.set(false); + this.updateRoom(rid, { hasMoreNext: false }); } } this.unqueue(); } public hasMore(rid: IRoom['_id']) { - const room = this.getRoom(rid); - return room.hasMore.get(); + return this.getRoom(rid).hasMore; } public hasMoreNext(rid: IRoom['_id']) { - const room = this.getRoom(rid); - return room.hasMoreNext.get(); + return this.getRoom(rid).hasMoreNext; } public getMoreIfIsEmpty(rid: IRoom['_id']) { @@ -276,8 +281,7 @@ class RoomHistoryManagerClass extends Emitter { } public isLoading(rid: IRoom['_id']) { - const room = this.getRoom(rid); - return room.isLoading.get(); + return this.getRoom(rid).isLoading; } public close(rid: IRoom['_id']) { @@ -288,11 +292,13 @@ class RoomHistoryManagerClass extends Emitter { public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); Messages.state.remove((record) => record.rid === rid); - room.isLoading.set(false); - room.hasMore.set(true); - room.hasMoreNext.set(false); room.oldestTs = undefined; room.loaded = undefined; + this.updateRoom(rid, { + isLoading: false, + hasMore: true, + hasMoreNext: false, + }); } public async getSurroundingMessages(message?: Pick & { ts?: Date }) { @@ -315,6 +321,7 @@ class RoomHistoryManagerClass extends Emitter { } const room = this.getRoom(message.rid); + this.updateRoom(message.rid, { isLoading: true }); const subscription = Subscriptions.state.find((record) => record.rid === message.rid); const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit, showThreadMessages); @@ -322,6 +329,7 @@ class RoomHistoryManagerClass extends Emitter { this.clear(message.rid); if (!result) { + this.updateRoom(message.rid, { isLoading: false }); return; } const { messages = [] } = result; @@ -332,18 +340,24 @@ class RoomHistoryManagerClass extends Emitter { await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); - Tracker.afterFlush(async () => { - this.emit('loaded-messages'); - room.isLoading.set(false); - }); + this.emit('loaded-messages'); + this.updateRoom(message.rid, { isLoading: false }); if (!room.loaded) { room.loaded = 0; } room.loaded += result.messages.length; - room.hasMore.set(result.moreBefore); - room.hasMoreNext.set(result.moreAfter); + this.updateRoom(message.rid, { + hasMore: result.moreBefore, + hasMoreNext: result.moreAfter, + }); } } export const RoomHistoryManager = new RoomHistoryManagerClass(); + +export const useRoomHistoryState = (rid: IRoom['_id'], selector: (state: RoomHistoryState) => T): T => + useSyncExternalStore( + useCallback((onStoreChange) => RoomHistoryManager.subscribeToRoom(rid, onStoreChange), [rid]), + () => selector(RoomHistoryManager.getRoom(rid)), + ); diff --git a/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts index 2360c69e354aa..4a45970bda38b 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts @@ -1,5 +1,6 @@ import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; + +import { absoluteUrl } from '../../../../../client/lib/absoluteUrl'; export class AudioEncoder extends Emitter { private worker: Worker; @@ -9,7 +10,7 @@ export class AudioEncoder extends Emitter { constructor(source: MediaStreamAudioSourceNode, { bufferLen = 4096, numChannels = 1, bitRate = 32 } = {}) { super(); - const workerPath = Meteor.absoluteUrl('workers/mp3-encoder/index.js'); + const workerPath = absoluteUrl('workers/mp3-encoder/index.js'); this.worker = new Worker(workerPath); this.worker.onmessage = this.handleWorkerMessage; diff --git a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts index f0a4e58f8a6f8..3ef71d274e547 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts +++ b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts @@ -1,18 +1,6 @@ import { VideoRecorder } from './videoRecorder'; import { createDeferredPromise } from '../../../../../tests/mocks/utils/createDeferredMockFn'; -jest.mock('meteor/reactive-var', () => ({ - ReactiveVar: jest.fn().mockImplementation((initialValue) => { - let value = initialValue; - return { - get: jest.fn(() => value), - set: jest.fn((newValue) => { - value = newValue; - }), - }; - }), -})); - describe('VideoRecorder', () => { let mockStream: MediaStream; let mockVideoTrack: MediaStreamTrack; @@ -93,7 +81,7 @@ describe('VideoRecorder', () => { streamDeferred.resolve(mockStream); await jest.runAllTimersAsync(); - expect(VideoRecorder.cameraStarted.get()).toBe(false); + expect(VideoRecorder.cameraStarted).toBe(false); }); it('should handle multiple start/stop cycles', async () => { @@ -115,7 +103,7 @@ describe('VideoRecorder', () => { await jest.runAllTimersAsync(); expect(cb).toHaveBeenCalledWith(true); - expect(VideoRecorder.cameraStarted.get()).toBe(true); + expect(VideoRecorder.cameraStarted).toBe(true); }); it('should invalidate pending callbacks from previous start when new start is called', async () => { @@ -155,19 +143,19 @@ describe('VideoRecorder', () => { await jest.runAllTimersAsync(); expect(cb).toHaveBeenCalledWith(true); - expect(VideoRecorder.cameraStarted.get()).toBe(true); + expect(VideoRecorder.cameraStarted).toBe(true); }); it('should stop camera tracks', () => { (VideoRecorder as any).stream = mockStream; (VideoRecorder as any).started = true; - VideoRecorder.cameraStarted.set(true); + (VideoRecorder as any)._cameraStarted = true; VideoRecorder.stop(); expect(mockVideoTrack.stop).toHaveBeenCalled(); expect(mockAudioTrack.stop).toHaveBeenCalled(); - expect(VideoRecorder.cameraStarted.get()).toBe(false); + expect(VideoRecorder.cameraStarted).toBe(false); }); it('should return supported mime types', () => { diff --git a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts index 0557f4706cd8b..53cd04eaa5be6 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts +++ b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts @@ -1,13 +1,16 @@ -import { ReactiveVar } from 'meteor/reactive-var'; +import { Emitter } from '@rocket.chat/emitter'; +import { useCallback, useSyncExternalStore } from 'react'; -class VideoRecorder { - public cameraStarted = new ReactiveVar(false); +type VideoRecorderEvents = { + cameraStartedChange: boolean; +}; - private started = false; +class VideoRecorder extends Emitter { + private _cameraStarted = false; - private recording = new ReactiveVar(false); + private started = false; - private recordingAvailable = new ReactiveVar(false); + private recordingAvailable = false; private videoel: HTMLVideoElement | undefined; @@ -19,6 +22,18 @@ class VideoRecorder { private sessionId = 0; + public get cameraStarted(): boolean { + return this._cameraStarted; + } + + private setCameraStarted(value: boolean) { + if (this._cameraStarted === value) { + return; + } + this._cameraStarted = value; + this.emit('cameraStartedChange', value); + } + public getSupportedMimeTypes() { if (window.MediaRecorder.isTypeSupported('video/webm')) { return 'video/webm; codecs=vp8,opus'; @@ -71,12 +86,11 @@ class VideoRecorder { this.mediaRecorder = new MediaRecorder(this.stream, { mimeType: this.getSupportedMimeTypes() }); this.mediaRecorder.ondataavailable = (blobev) => { this.chunks.push(blobev.data); - if (!this.recordingAvailable.get()) { - return this.recordingAvailable.set(true); + if (!this.recordingAvailable) { + this.recordingAvailable = true; } }; this.mediaRecorder.start(); - return this.recording.set(true); } private stopStreamTracks(stream: MediaStream) { @@ -109,7 +123,7 @@ class VideoRecorder { } this.started = true; - return this.cameraStarted.set(true); + this.setCameraStarted(true); } public stop(cb?: (blob: Blob) => void) { @@ -128,8 +142,8 @@ class VideoRecorder { const wasStarted = this.started; this.started = false; - this.cameraStarted.set(false); - this.recordingAvailable.set(false); + this.setCameraStarted(false); + this.recordingAvailable = false; if (cb && this.chunks && wasStarted) { const blob = new Blob(this.chunks); @@ -141,12 +155,11 @@ class VideoRecorder { } public stopRecording() { - if (!this.started || !this.recording || !this.mediaRecorder) { + if (!this.started || !this.mediaRecorder) { return; } this.mediaRecorder.stop(); - this.recording.set(false); delete this.mediaRecorder; } } @@ -154,3 +167,9 @@ class VideoRecorder { const instance = new VideoRecorder(); export { instance as VideoRecorder }; + +export const useVideoRecorderCameraStarted = (): boolean => + useSyncExternalStore( + useCallback((onStoreChange) => instance.on('cameraStartedChange', onStoreChange), []), + () => instance.cameraStarted, + ); diff --git a/apps/meteor/app/utils/client/getRoomAvatarURL.ts b/apps/meteor/app/utils/client/getRoomAvatarURL.ts index 5abf77faa59b1..b15588bed486c 100644 --- a/apps/meteor/app/utils/client/getRoomAvatarURL.ts +++ b/apps/meteor/app/utils/client/getRoomAvatarURL.ts @@ -4,7 +4,7 @@ import { getAvatarURL } from './getAvatarURL'; import { settings } from '../../../client/lib/settings'; export const getRoomAvatarURL = ({ roomId, cache = '' }: { roomId: IRoom['_id']; cache: IRoom['avatarETag'] }) => { - const externalSource = (settings.watch('Accounts_RoomAvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); + const externalSource = (settings.peek('Accounts_RoomAvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); if (externalSource && typeof externalSource === 'string') { return externalSource.replace('{roomId}', roomId); } diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index b427c7110ccef..0a292e6009589 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -14,8 +14,8 @@ export const getURL = function ( cloudDeepLinkUrl?: string, cacheKey?: boolean, ): string { - const cdnPrefix = settings.watch('CDN_PREFIX') || ''; - const siteUrl = settings.watch('Site_Url') || ''; + const cdnPrefix = settings.peek('CDN_PREFIX') || ''; + const siteUrl = settings.peek('Site_Url') || ''; if (cacheKey) { path += `${path.includes('?') ? '&' : '?'}cacheKey=${Info.version}`; diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index 3ea1d39f4f2f6..7cdb81beb7835 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -1,9 +1,9 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { RestClient } from '@rocket.chat/api-client'; -import { Accounts } from 'meteor/accounts-base'; import { invokeTwoFactorModal } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; +import { STORAGE_KEYS, getStoredItem } from '../../../../client/lib/sdk/storage'; class RestApiClient extends RestClient { override getCredentials(): @@ -12,10 +12,7 @@ class RestApiClient extends RestClient { 'X-Auth-Token': string; } | undefined { - const [uid, token] = [ - Accounts.storageLocation.getItem(Accounts.USER_ID_KEY), - Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY), - ]; + const [uid, token] = [getStoredItem(STORAGE_KEYS.USER_ID), getStoredItem(STORAGE_KEYS.LOGIN_TOKEN)]; if (!uid || !token) { return; diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index d064f52e9ef74..60ac7001fc08d 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -1,11 +1,12 @@ import type { RestClientInterface } from '@rocket.chat/api-client'; import type { SDK, ClientStream, StreamKeys, StreamNames, StreamerCallbackArgs, ServerMethods } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; -import { Accounts } from 'meteor/accounts-base'; -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; import { APIClient } from './RestApiClient'; +import { parseDDP } from '../../../../client/lib/sdk/ddpProtocol'; +import { ensureConnectedAndAuthenticated, getDdpSdk } from '../../../../client/lib/sdk/ddpSdk'; +import { isSdkTransportEnabled } from '../../../../client/lib/sdk/sdkTransportEnabled'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,10 +16,13 @@ declare module '@rocket.chat/ddp-client' { args: [key: K, ...args: unknown[]], callback: (...args: StreamerCallbackArgs) => void, ): ReturnType; + onAnyStreamEvent(name: N, callback: (eventName: string, args: unknown[]) => void): { stop: () => void }; call(method: T, ...args: Parameters): Promise>; } } +const sdkTransportEnabled = isSdkTransportEnabled(); + const isChangedCollectionPayload = ( msg: any, ): msg is { msg: 'changed'; collection: string; fields: { eventName: string; args: unknown[] } } => { @@ -142,28 +146,135 @@ const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys, + streamName: StreamNames, + key: StreamKeys, + args: unknown[], +): StreamMapValue => { + const ee = new Emitter<{ + ready: [error: any] | [undefined, any]; + error: [error: any]; + stop: undefined; + }>(); + const meta = { ready: false }; + + // Defer the actual `subscribe` until DDPSDK is authenticated. Without this, + // stream subscriptions fired immediately after re-login (e.g. the + // SubscriptionsCachedStore's `notify-user//subscriptions-changed` + // listener that re-arms via onLoggedIn) hit the SDK socket while it's + // still anonymous — server rejects with `not-allowed`/`nosub`, the + // stream's `ready` promise emits an error, and the cached store never + // receives subsequent server events. The visible failure: an agent that + // just took a livechat chat post-relogin sees the chat work but the + // "Move to the queue" button never appears, because the new subscription + // the server creates for that agent is never replicated to the client's + // Subscriptions store, and pseudoRoom (= {...sub, ...room}) ends up with + // no `u` for the canMoveQueue check. + let subscription: ReturnType['client']['subscribe']> | undefined; + let offCollection: (() => void) | undefined; + let stopped = false; + + void ensureConnectedAndAuthenticated() + .catch(() => undefined) + .then(() => { + if (stopped) return; + const sdk = getDdpSdk(); + subscription = sdk.client.subscribe(`stream-${streamName}`, key, { useCollection: false, args }); + + subscription + .ready() + .then(() => { + if (stopped) return; + meta.ready = true; + ee.emit('ready', [undefined, { msg: 'ready', subs: [subscription!.id] }]); + }) + .catch((err) => { + if (stopped) return; + ee.emit('ready', [err]); + ee.emit('error', err); + }); + + offCollection = sdk.client.onCollection(`stream-${streamName}`, (data: any) => { + if (data?.msg !== 'changed') return; + if (data.collection !== `stream-${streamName}`) return; + if (data.fields?.eventName !== key) return; + streamProxy.emit(`stream-${streamName}/${key}`, data.fields.args); + }); + }); + + const onChange: ReturnType['onChange'] = (cb) => { + if (meta.ready) { + cb({ msg: 'ready', subs: [] }); + return; + } + ee.once('ready', ([error, result]) => { + if (error) { + cb({ msg: 'nosub', id: '', error }); + return; + } + cb(result); + }); + }; + + return { + stop: () => { + // Mirror Meteor's subscription semantics: explicit stop() does not fire the + // 'stop' event (onStop is reserved for server-initiated closures). + // Emitting it here would recurse through the onStop handler that + // createStreamManager registers, which itself iterates the unsubList. + stopped = true; + offCollection?.(); + subscription?.stop(); + }, + onChange, + ready: () => { + if (meta.ready) return Promise.resolve(); + return new Promise((resolve, reject) => { + ee.once('ready', ([err]) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + }, + onError: (cb: (...args: any[]) => void) => ee.once('error', (error) => cb(error)), + onStop: (cb: () => void) => ee.once('stop', cb), + get isReady() { + return meta.ready; + }, + unsubList: new Set(), + }; +}; + const createStreamManager = () => { // Emitter that replicates stream messages to registered callbacks const streamProxy = new Emitter(); - // Collection of unsubscribe callbacks for each stream. - // const proxyUnsubLists = new Map void>>(); - const streams = new Map(); - Accounts.onLogout(() => { + getDdpSdk().account.onLogout(() => { streams.forEach((stream) => { stream.unsubList.forEach((stop) => stop()); }); }); - Meteor.connection._stream.on('message', (rawMsg: string) => { - const msg = DDPCommon.parseDDP(rawMsg); - if (!isChangedCollectionPayload(msg)) { - return; - } - streamProxy.emit(`${msg.collection}/${msg.fields.eventName}` as any, msg.fields.args as any); - }); + if (!sdkTransportEnabled) { + // In legacy Meteor mode, stream frames arrive on Meteor.connection._stream + // as `changed` collection messages — bridge them into streamProxy so the + // per-stream callbacks fire. With SDK transport on, the frames arrive on + // the SDK socket and createNewDdpSdkStream registers its own onCollection + // listener instead. + Meteor.connection._stream!.on('message', (rawMsg: string) => { + const msg = parseDDP(rawMsg); + if (!isChangedCollectionPayload(msg)) { + return; + } + streamProxy.emit(`${msg.collection}/${msg.fields.eventName}` as any, msg.fields.args as any); + }); + } const stream: SDK['stream'] = >( name: N, @@ -186,7 +297,11 @@ const createStreamManager = () => { streamProxy.on(eventLiteral, proxyCallback); - const stream = streams.get(eventLiteral) || createNewMeteorStream(name, key, args); + const stream = + streams.get(eventLiteral) || + (sdkTransportEnabled + ? createNewDdpSdkStream(streamProxy, name, key as StreamKeys, args) + : createNewMeteorStream(name, key as StreamKeys, args)); const stop = (): void => { streamProxy.off(eventLiteral, proxyCallback); @@ -238,33 +353,105 @@ const createStreamManager = () => { return { stream, stopAll }; }; +// Per-stream wildcard emitters for `onAnyStreamEvent`. Each emitter is fed by +// up to two sources (de-duplicated by the underlying bridges): +// 1) `getDdpSdk().client.onCollection(...)` — the canonical bridge. Covers +// both transport-OFF (via `meteorBackedSdk.onCollection`, which listens +// on `Meteor.connection._stream`) and transport-ON (via the real DDPSDK +// socket). +// 2) When SDK transport is ON, a direct `Meteor.connection._stream` bridge +// to catch frames that land on Meteor while the SDK socket is still +// authenticating — see `apps/meteor/client/lib/presence.ts`'s fallback +// to `Meteor.subscribe('stream-user-presence', ...)`. This is the +// "artificial trigger" and is TEMPORARY: it disappears once SDK +// transport rollout completes and the Meteor fallback is removed. +// +// Bridges are wired exactly once per stream name (singleton listeners on the +// underlying Meteor stream — Meteor's `on()` has no `off()`, so we can't +// detach them per subscription anyway). Consumers register/unregister on the +// per-stream Emitter, which DOES support `off`, so unsubscription is clean. +const anyStreamEmitters = new Map>>(); +const anyStreamBridged = new Set(); + +const createOnAnyStreamEvent = () => { + return (name: N, callback: (eventName: string, args: unknown[]) => void): { stop: () => void } => { + const collectionId = `stream-${name}`; + + let emitter = anyStreamEmitters.get(collectionId); + if (!emitter) { + emitter = new Emitter>(); + anyStreamEmitters.set(collectionId, emitter); + } + + if (!anyStreamBridged.has(collectionId)) { + anyStreamBridged.add(collectionId); + const bridgeEmitter = emitter; + + // Primary bridge — works for both transport-OFF (meteorBackedSdk's + // onCollection listens on Meteor.connection._stream) and transport-ON + // (real DDPSDK socket). + getDdpSdk().client.onCollection(collectionId, (data: unknown) => { + if (!isChangedCollectionPayload(data)) return; + if (data.collection !== collectionId) return; + bridgeEmitter.emit('event', [data.fields.eventName, data.fields.args]); + }); + + // Temporary secondary bridge — see block comment above. Only needed + // while SDK transport is on AND Meteor.connection may still receive + // frames during the SDK socket's anonymous window. + if (sdkTransportEnabled) { + Meteor.connection._stream!.on('message', (rawMsg: string) => { + let msg: unknown; + try { + msg = parseDDP(rawMsg); + } catch { + return; + } + if (!isChangedCollectionPayload(msg)) return; + if (msg.collection !== collectionId) return; + bridgeEmitter.emit('event', [msg.fields.eventName, msg.fields.args]); + }); + } + } + + const handler = ([eventName, args]: [string, unknown[]]): void => { + callback(eventName, args); + }; + emitter.on('event', handler); + + return { + stop: () => { + emitter.off('event', handler); + }, + }; + }; +}; + export const createSDK = (rest: RestClientInterface) => { const { stream, stopAll } = createStreamManager(); + const onAnyStreamEvent = createOnAnyStreamEvent(); - const publish = (name: string, args: unknown[]) => { - Meteor.call(`stream-${name}`, ...args); - }; + const publish = sdkTransportEnabled + ? (name: string, args: unknown[]) => { + // DDPSDK queues outbound frames until the WebSocket handshake completes, + // so there's no need to gate on an isReady flag here. + void getDdpSdk().client.callAsync(`stream-${name}`, ...args); + } + : (name: string, args: unknown[]) => { + Meteor.call(`stream-${name}`, ...args); + }; const call = (method: T, ...args: Parameters): Promise> => { return Meteor.callAsync(method, ...args); }; - const disconnect = () => { - Meteor.disconnect(); - }; - - const reconnect = () => { - Meteor.reconnect(); - }; - return { rest, stop: stopAll, stream, + onAnyStreamEvent, publish, call, - disconnect, - reconnect, }; }; diff --git a/apps/meteor/app/utils/client/lib/getUserPreference.ts b/apps/meteor/app/utils/client/lib/getUserPreference.ts index ac3aec3e0c5ac..0e523fad8d3e2 100644 --- a/apps/meteor/app/utils/client/lib/getUserPreference.ts +++ b/apps/meteor/app/utils/client/lib/getUserPreference.ts @@ -43,5 +43,5 @@ export function getUserPreference( defaultValue?: TValue, ): TValue { const user = typeof userIdOrUser === 'string' ? Users.state.get(userIdOrUser) : userIdOrUser; - return user?.settings?.preferences?.[key] ?? defaultValue ?? settings.watch(`Accounts_Default_User_Preferences_${key}`); + return user?.settings?.preferences?.[key] ?? defaultValue ?? settings.peek(`Accounts_Default_User_Preferences_${key}`); } diff --git a/apps/meteor/app/utils/client/restrictions.ts b/apps/meteor/app/utils/client/restrictions.ts index 4e55bb1a52b90..84a1db0272d8d 100644 --- a/apps/meteor/app/utils/client/restrictions.ts +++ b/apps/meteor/app/utils/client/restrictions.ts @@ -2,8 +2,8 @@ import { settings } from '../../../client/lib/settings'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { - const blackList = settings.watch('FileUpload_MediaTypeBlackList') ?? 'image/svg+xml'; - const whiteList = customWhiteList ?? settings.watch('FileUpload_MediaTypeWhiteList') ?? ''; + const blackList = settings.peek('FileUpload_MediaTypeBlackList') ?? 'image/svg+xml'; + const whiteList = customWhiteList ?? settings.peek('FileUpload_MediaTypeWhiteList') ?? ''; return fileUploadIsValidContentTypeFromSettings(type, whiteList, blackList); }; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 2f8592690a8f2..627c6d80d5d52 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.4.3" + "version": "8.5.0-rc.6" } diff --git a/apps/meteor/app/utils/server/functions/isDocker.ts b/apps/meteor/app/utils/server/functions/isDocker.ts index 86471cec24d1f..56d074718d069 100644 --- a/apps/meteor/app/utils/server/functions/isDocker.ts +++ b/apps/meteor/app/utils/server/functions/isDocker.ts @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'node:fs'; function hasDockerEnv(): boolean { try { diff --git a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts index 60bacbe71c9d4..2d0ce22a3c427 100644 --- a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts +++ b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts @@ -1,4 +1,4 @@ -import os from 'os'; +import os from 'node:os'; import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; diff --git a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts b/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts index 43e9bb4642d77..c3d7df2330509 100644 --- a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts +++ b/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts @@ -1,5 +1,5 @@ -import stream from 'stream'; -import type { Readable, Writable } from 'stream'; +import stream from 'node:stream'; +import type { Readable, Writable } from 'node:stream'; import type { WebDAVClient, FileStat, ResponseDataDetailed, WebDAVClientOptions } from 'webdav'; import { createClient } from 'webdav'; diff --git a/apps/meteor/client/apps/RealAppsEngineUIHost.ts b/apps/meteor/client/apps/RealAppsEngineUIHost.ts index c6a746f6bb7a8..a8da21e8aeea1 100644 --- a/apps/meteor/client/apps/RealAppsEngineUIHost.ts +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.ts @@ -1,5 +1,6 @@ -import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; -import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; +import { AppsEngineUIHost } from '@rocket.chat/apps/dist/client/AppsEngineUIHost'; +import type { IExternalComponentRoomInfo } from '@rocket.chat/apps/dist/client/definition/IExternalComponentRoomInfo'; +import type { IExternalComponentUserInfo } from '@rocket.chat/apps/dist/client/definition/IExternalComponentUserInfo'; import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; import { sdk } from '../../app/utils/client/lib/SDKClient'; diff --git a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index e40afaf35f438..e3a6f13271d01 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -1,7 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; +import { Random } from '@rocket.chat/random'; import { GenericModal } from '@rocket.chat/ui-client'; -import { Tracker } from 'meteor/tracker'; import type { ReactElement } from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,19 +34,13 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer roomCoordinator.openRouteLink(result.t, result); - Tracker.autorun((c) => { - if (openedRoom !== result.rid) { - return; - } - + if (openedRoom === result.rid) { callWithErrorHandling('sendMessage', { _id: Random.id(), rid: result.rid, msg: t('Apps_Game_Center_Play_Game_Together', { name }), }); - - c.stop(); - }); + } onClose(); } catch (err) { console.warn(err); diff --git a/apps/meteor/client/apps/orchestrator.ts b/apps/meteor/client/apps/orchestrator.ts index d08641da34d22..e376211b8260a 100644 --- a/apps/meteor/client/apps/orchestrator.ts +++ b/apps/meteor/client/apps/orchestrator.ts @@ -1,5 +1,5 @@ -import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; -import type { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; +import { AppClientManager } from '@rocket.chat/apps/dist/client/AppClientManager'; +import type { AppsEngineUIHost } from '@rocket.chat/apps/dist/client/AppsEngineUIHost'; import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; diff --git a/apps/meteor/client/cachedStores/RoomsCachedStore.ts b/apps/meteor/client/cachedStores/RoomsCachedStore.ts index 85e78c9041b4f..f02ec85cced80 100644 --- a/apps/meteor/client/cachedStores/RoomsCachedStore.ts +++ b/apps/meteor/client/cachedStores/RoomsCachedStore.ts @@ -1,5 +1,5 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; -import { DEFAULT_SLA_CONFIG, isABACManagedRoom, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings'; +import { DEFAULT_SLA_CONFIG, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { PrivateCachedStore } from '../lib/cachedStores/CachedStore'; @@ -53,9 +53,7 @@ class RoomsCachedStore extends PrivateCachedStore { source: (room as IOmnichannelRoom | undefined)?.source, queuedAt: (room as IOmnichannelRoom | undefined)?.queuedAt, federated: room.federated, - ...(isABACManagedRoom(room) && { - abacAttributes: room.abacAttributes, - }), + abacAttributes: room.abacAttributes, ...(isRoomNativeFederated(room) && { federation: room.federation, }), diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index 2a6964462deaf..6a41769ef5566 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -1,22 +1,21 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { Icon, Box } from '@rocket.chat/fuselage'; import { Field, FieldGroup, ToggleSwitch, TextInput, TextAreaInput, - Icon, - Box, FieldHint, FieldLabel, FieldRow, FieldError, -} from '@rocket.chat/fuselage'; +} from '@rocket.chat/fuselage-forms'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericModal } from '@rocket.chat/ui-client'; import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; -import { useId, useState } from 'react'; +import { useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useEncryptedRoomDescription } from '../../navbar/NavBarPagesGroup/actions/useEncryptedRoomDescription'; @@ -105,13 +104,6 @@ const CreateDiscussion = ({ const getEncryptedHint = useEncryptedRoomDescription('discussion'); - const parentRoomId = useId(); - const encryptedId = useId(); - const discussionNameId = useId(); - const membersId = useId(); - const firstMessageId = useId(); - const topicId = useId(); - return ( {t('Discussion_description')} - - {t('Discussion_target_channel')} - + {t('Discussion_target_channel')} {defaultParentRoom && ( } + render={() => } /> )} {!defaultParentRoom && ( @@ -142,18 +132,13 @@ const CreateDiscussion = ({ control={control} name='parentRoom' rules={{ required: t('Required_field', { field: t('Discussion_target_channel') }) }} - render={({ field: { name, onBlur, onChange, value } }) => ( + render={({ field }) => ( (encrypted ? : null)} /> @@ -161,106 +146,66 @@ const CreateDiscussion = ({ /> )} - {errors.parentRoom && ( - - {errors.parentRoom.message} - - )} + {errors.parentRoom && {errors.parentRoom.message}} - - {t('Name')} - + {t('Name')} ( - } - /> + } error={errors.name?.message} /> )} /> - {errors.name && ( - - {errors.name.message} - - )} + {errors.name && {errors.name.message}} - {t('Topic')} + {t('Topic')} - } - /> + } /> - {t('Displayed_next_to_name')} + {t('Displayed_next_to_name')} - {t('Members')} + {t('Members')} ( - - )} + render={({ field }) => } /> - {t('Discussion_first_message_title')} + {t('Discussion_first_message_title')} ( - - )} + render={({ field }) => } /> {encrypted ? ( - {t('Discussion_first_message_disabled_due_to_e2e')} + {t('Discussion_first_message_disabled_due_to_e2e')} ) : ( - {t('First_message_hint')} + {t('First_message_hint')} )} - {t('Encrypted')} + {t('Encrypted')} ( - - )} + render={({ field: { value, ...field } }) => } /> - {getEncryptedHint({ isPrivate: true, encrypted })} + {getEncryptedHint({ isPrivate: true, encrypted })} diff --git a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx index b34940ab5bd4b..9d1ca1a65661a 100644 --- a/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx +++ b/apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx @@ -1,4 +1,5 @@ -import { Skeleton, TextInput, Callout } from '@rocket.chat/fuselage'; +import { Skeleton, Callout } from '@rocket.chat/fuselage'; +import { TextInput } from '@rocket.chat/fuselage-forms'; import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ComponentPropsWithoutRef } from 'react'; diff --git a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap index 5691fe063e502..f79ddc71ebcc7 100644 --- a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap +++ b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap @@ -4,7 +4,7 @@ exports[`CreateDiscussion renders Default without crashing 1`] = `

Discussion_title

@@ -61,9 +61,9 @@ exports[`CreateDiscussion renders Default without crashing 1`] = `
- + @@ -84,11 +84,12 @@ exports[`CreateDiscussion renders Default without crashing 1`] = ` role="group" >