Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/ddp-migrate-batch2-callers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@rocket.chat/meteor': patch
---

Migrate six client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them):

- `loadMissedMessages` → `GET /v1/chat.syncMessages`
- `joinRoom` → `POST /v1/channels.join` (channel-only; non-`c` rooms now error via REST the same way they used to via DDP)
- `userSetUtcOffset` → `POST /v1/users.setPreferences` (new `utcOffset` field)
- `deleteFileMessage` → `POST /v1/chat.delete` (new `fileId` body shape)
- `spotlight` → `GET /v1/spotlight` (new `usernames` / `type` / `rid` query params)
- `listCustomSounds` → `GET /v1/custom-sounds.list`
6 changes: 6 additions & 0 deletions .changeset/rest-chat-delete-by-fileid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

`POST /v1/chat.delete` now accepts `{ fileId, asUser? }` as an alternative to `{ msgId, roomId, asUser? }`. When `fileId` is provided the server resolves the owning message via `Messages.getMessageByFileId` before running the existing permission and deletion flow.
11 changes: 11 additions & 0 deletions .changeset/rest-spotlight-params-and-anonymous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

`GET /v1/spotlight` now mirrors the DDP `spotlight` method:

- accepts optional `usernames` (comma-separated string), `type` (JSON-encoded `{ users?, mentions?, rooms?, includeFederatedRooms? }`) and `rid` query params;
- response items expose `nickname` / `outside` (users) and `uids` / `usernames` / `fname` (rooms);
- `status` on each user is now optional — outside/federated users were already being returned without one and the previous required-field schema rejected them as `Response validation failed`;
- the endpoint is no longer auth-gated, allowing anonymous-read flows (e.g. `Accounts_AllowAnonymousRead`) to keep finding public channels through the navbar search.
6 changes: 6 additions & 0 deletions .changeset/rest-users-setpreferences-utcoffset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

`POST /v1/users.setPreferences` now accepts an optional `data.utcOffset` (number) field. The value is stored at the user-document root via `Users.setUtcOffset` (not under `settings.preferences`), matching what the legacy `userSetUtcOffset` DDP method did.
19 changes: 14 additions & 5 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ const chatEndpoints = API.v1
authRequired: true,
body: isChatDeleteProps,
response: {
200: ajv.compile<{ _id: string; ts: string; message: Pick<IMessage, '_id' | 'rid' | 'u'> }>({
200: ajv.compile<{ _id?: string; ts?: string; message?: Pick<IMessage, '_id' | 'rid' | 'u'> }>({
type: 'object',
properties: {
_id: { type: 'string' },
Expand All @@ -538,21 +538,30 @@ const chatEndpoints = API.v1
},
success: { type: 'boolean', enum: [true] },
},
required: ['_id', 'ts', 'message', 'success'],
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });
// Deleting by fileId resolves the message that references the file and deletes it.
// An orphan upload (a file with no associated message) is not deletable through this
// endpoint and intentionally returns a failure below.
const msg =
'fileId' in this.bodyParams
? await Messages.getMessageByFileId(this.bodyParams.fileId)
: await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });

Comment on lines +553 to 556

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Reject requests that include both fileId and msgId to avoid ambiguous delete-target resolution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/chat.ts, line 550:

<comment>Reject requests that include both `fileId` and `msgId` to avoid ambiguous delete-target resolution.</comment>

<file context>
@@ -546,13 +546,17 @@ const chatEndpoints = API.v1
 		async function action() {
-			const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });
+			const msg =
+				'fileId' in this.bodyParams
+					? await Messages.getMessageByFileId(this.bodyParams.fileId)
+					: await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });
</file context>
Suggested change
'fileId' in this.bodyParams
? await Messages.getMessageByFileId(this.bodyParams.fileId)
: await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });
if ('fileId' in this.bodyParams && 'msgId' in this.bodyParams) {
return API.v1.failure('Provide either "fileId" or "msgId"/"roomId", not both.');
}
const msg =
'fileId' in this.bodyParams
? await Messages.getMessageByFileId(this.bodyParams.fileId)
: await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } });

if (!msg) {
if ('fileId' in this.bodyParams) {
return API.v1.failure(`No message found with the file id: "${this.bodyParams.fileId}".`);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`);
}

if (this.bodyParams.roomId !== msg.rid) {
if ('roomId' in this.bodyParams && this.bodyParams.roomId !== msg.rid) {
Comment on lines +552 to +564

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium: Missing Authorization Check in chat.starMessage/unStarMessage

The chat.starMessage and chat.unStarMessage endpoints allow a user to star or unstar any message by providing its messageId. The implementation retrieves the message from the database and passes it to the starMessage function without verifying if the authenticated user has access to the room where the message resides. An attacker could potentially star/unstar messages in rooms they are not authorized to access.

Trace
graph TD
    subgraph SG0 ["apps/meteor/app/2fa/server/code/index.ts"]
        checkCodeForUser["checkCodeForUser"]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/api/server/ApiClass.ts"]
        generateConnection["Generates a connection object for API requests."]
        APIClass.shouldAddRateLimitToRoute["APIClass.shouldAddRateLimitToRoute"]
        APIClass.success["Formats a successful API response."]
        APIClass.failure["APIClass.failure"]
        APIClass.internalError["Formats an internal server error API response."]
        APIClass.unauthorized["Formats an unauthorized API response."]
        APIClass.forbidden["APIClass.forbidden"]
        APIClass.tooManyRequests["APIClass.tooManyRequests"]
        APIClass.shouldVerifyRateLimit["APIClass.shouldVerifyRateLimit"]
        APIClass.enforceRateLimit["APIClass.enforceRateLimit"]
        APIClass.addRateLimiterRuleForRoutes["APIClass.addRateLimiterRuleForRoutes"]
        APIClass.processTwoFactor["APIClass.processTwoFactor"]
        APIClass.getFullRouteName["APIClass.getFullRouteName"]
        APIClass.namedRoutes["APIClass.namedRoutes"]
        APIClass.registerTypedRoutesLegacy["APIClass.registerTypedRoutesLegacy"]
        APIClass.registerTypedRoutes["APIClass.registerTypedRoutes"]
        APIClass.method["Internal helper to register an API method."]
        APIClass.get["Registers a GET API route."]
        APIClass.post["Registers a POST API route."]
        APIClass.addRoute["APIClass.addRoute"]
        APIClass.this.parseJsonQuery["APIClass.this.parseJsonQuery"]
        APIClass.authenticatedRoute["APIClass.authenticatedRoute"]
        APIClass.loginCompatibility["Adapts login credentials for compatibility."]
        APIClass.createMeteorInvocation["APIClass.createMeteorInvocation"]
        APIClass.applyInvocation["APIClass.applyInvocation"]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/api/server/api.helpers.ts"]
        isLegacyPermissionsPayload["isLegacyPermissionsPayload"]
        isLightPermissionsPayload["isLightPermissionsPayload"]
        isPermissionsPayload["isPermissionsPayload"]
        checkPermissionsForInvocation["checkPermissionsForInvocation"]
        checkPermissions["checkPermissions"]
        parseDeprecation["parseDeprecation"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/api/server/helpers/getUserInfo.ts"]
        isVerifiedEmail["isVerifiedEmail"]
        getUserPreferences["getUserPreferences"]
        filterOutdatedVersionUpdateBanners["filterOutdatedVersionUpdateBanners"]
        getUserCalendar["getUserCalendar"]
        getUserInfo["Constructs user information object for API responses."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/api/server/middlewares/authenticationHono.ts"]
        isUserWithUsername["isUserWithUsername"]
        authenticationMiddlewareForHono["authenticationMiddlewareForHono"]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/app/api/server/middlewares/permissions.ts"]
        permissionsMiddleware["permissionsMiddleware"]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/app/api/server/router.ts"]
        convertHonoContextToApiActionContext["convertHonoContextToApiActionContext"]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/app/api/server/v1/chat.ts"]
        ._Rocket.Chat_apps_meteor_app_api_server_v1_chat.ts{{"Registers and implements REST API endpoints for chat operations such as pinning, updating, starring, following messages, reacting, reporting, deleting, searching, and managing threads."}}
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts"]
        endpoint["endpoint"]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/app/utils/lib/getURL.ts"]
        getURL_2["_getURL"]
        getURLWithoutSettings["getURLWithoutSettings"]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["apps/meteor/app/utils/server/functions/getBaseUserFields.ts"]
        getBaseUserFields["getBaseUserFields"]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG11 ["apps/meteor/app/utils/server/functions/getDefaultUserFields.ts"]
        getDefaultUserFields["Returns the default database fields allowed for user objects."]
    end
    style SG11 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG12 ["apps/meteor/app/utils/server/getURL.ts"]
        getURL["getURL"]
    end
    style SG12 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG13 ["apps/meteor/app/utils/server/lib/getUserPreference.ts"]
        getUserPreference["getUserPreference"]
    end
    style SG13 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG14 ["apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts"]
        license["license"]
    end
    style SG14 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG15 ["apps/meteor/lib/utils/isObject.ts"]
        isObject["isObject"]
    end
    style SG15 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG16 ["apps/meteor/server/lib/shouldBreakInVersion.ts"]
        shouldBreakInVersion["shouldBreakInVersion"]
    end
    style SG16 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG17 ["ee/apps/account-service/src/lib/utils.ts"]
        hashLoginToken["_hashLoginToken"]
    end
    style SG17 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG18 ["packages/logger/src/index.ts"]
        Logger.error["Logger.error"]
    end
    style SG18 fill:#2a2a2a,stroke:#444,color:#aaa
    ._Rocket.Chat_apps_meteor_app_api_server_v1_chat.ts --> APIClass.get
    ._Rocket.Chat_apps_meteor_app_api_server_v1_chat.ts --> APIClass.post
    APIClass.get --> APIClass.method
    APIClass.post --> getUserInfo
    APIClass.post --> generateConnection
    APIClass.post --> APIClass.success
    APIClass.post --> APIClass.internalError
    APIClass.post --> APIClass.unauthorized
    APIClass.post --> APIClass.method
    APIClass.post --> APIClass.loginCompatibility
    APIClass.post --> getDefaultUserFields
    APIClass.method --> APIClass.registerTypedRoutes
    APIClass.method --> APIClass.addRoute
    getUserInfo --> isVerifiedEmail
    getUserInfo --> getUserPreferences
    getUserInfo --> filterOutdatedVersionUpdateBanners
    getUserInfo --> getUserCalendar
    getUserInfo --> getURL
    APIClass.success --> isObject
    APIClass.loginCompatibility --> APIClass.get
    getDefaultUserFields --> getBaseUserFields
    APIClass.addRoute --> hashLoginToken
    APIClass.addRoute --> shouldBreakInVersion
    APIClass.addRoute --> generateConnection
    APIClass.addRoute --> APIClass.shouldAddRateLimitToRoute
    APIClass.addRoute --> APIClass.failure
    APIClass.addRoute --> APIClass.unauthorized
    APIClass.addRoute --> APIClass.forbidden
    APIClass.addRoute --> APIClass.tooManyRequests
    APIClass.addRoute --> APIClass.enforceRateLimit
    APIClass.addRoute --> APIClass.addRateLimiterRuleForRoutes
    APIClass.addRoute --> APIClass.processTwoFactor
    APIClass.addRoute --> APIClass.getFullRouteName
    APIClass.addRoute --> APIClass.registerTypedRoutesLegacy
    APIClass.addRoute --> APIClass.get
    APIClass.addRoute --> APIClass.this.parseJsonQuery
    APIClass.addRoute --> APIClass.createMeteorInvocation
    APIClass.addRoute --> APIClass.applyInvocation
    APIClass.addRoute --> authenticationMiddlewareForHono
    APIClass.addRoute --> permissionsMiddleware
    APIClass.addRoute --> checkPermissions
    APIClass.addRoute --> parseDeprecation
    APIClass.addRoute --> license
    getUserPreferences --> getUserPreference
    getURL --> getURLWithoutSettings
    APIClass.failure --> isObject
    APIClass.enforceRateLimit --> APIClass.shouldVerifyRateLimit
    APIClass.addRateLimiterRuleForRoutes --> APIClass.namedRoutes
    APIClass.processTwoFactor --> APIClass.get
    APIClass.processTwoFactor --> checkCodeForUser
    APIClass.registerTypedRoutesLegacy --> APIClass.registerTypedRoutes
    APIClass.this.parseJsonQuery --> APIClass.this.parseJsonQuery
    authenticationMiddlewareForHono --> APIClass.unauthorized
    authenticationMiddlewareForHono --> APIClass.get
    authenticationMiddlewareForHono --> APIClass.authenticatedRoute
    authenticationMiddlewareForHono --> convertHonoContextToApiActionContext
    authenticationMiddlewareForHono --> isUserWithUsername
    permissionsMiddleware --> APIClass.internalError
    permissionsMiddleware --> APIClass.unauthorized
    permissionsMiddleware --> APIClass.forbidden
    permissionsMiddleware --> APIClass.get
    permissionsMiddleware --> checkPermissionsForInvocation
    permissionsMiddleware --> Logger.error
    checkPermissions --> isLegacyPermissionsPayload
    checkPermissions --> isLightPermissionsPayload
    checkPermissions --> isPermissionsPayload
    parseDeprecation --> endpoint
    getURLWithoutSettings --> getURL_2
    APIClass.shouldVerifyRateLimit --> APIClass.get
    APIClass.namedRoutes --> APIClass.getFullRouteName
    APIClass.authenticatedRoute --> hashLoginToken
    APIClass.authenticatedRoute --> APIClass.method
    APIClass.authenticatedRoute --> APIClass.get
    APIClass.authenticatedRoute --> getDefaultUserFields
    Logger.error --> Logger.error
Loading
Fix with AI

Open in Cursor Open in Claude

Fix the following security vulnerability found by Hacktron.

File: apps/meteor/app/api/server/v1/chat.ts
Lines: 549-559
Severity: medium

Vulnerability: Missing Authorization Check in chat.starMessage/unStarMessage

Description:
The `chat.starMessage` and `chat.unStarMessage` endpoints allow a user to star or unstar any message by providing its `messageId`. The implementation retrieves the message from the database and passes it to the `starMessage` function without verifying if the authenticated user has access to the room where the message resides. An attacker could potentially star/unstar messages in rooms they are not authorized to access.

Fix this vulnerability. Only change what's necessary - don't modify unrelated code.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing. Any other reply is saved as a triage note.

View finding in Hacktron

return API.v1.failure('The room id provided does not match where the message is from.');
}

Expand All @@ -576,7 +585,7 @@ const chatEndpoints = API.v1
return API.v1.success({
_id: msg._id,
ts: Date.now().toString(),
message: msg,
message: { _id: msg._id, rid: msg.rid, u: msg.u },
});
},
)
Expand Down
25 changes: 20 additions & 5 deletions apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
ajv,
isShieldSvgProps,
isSpotlightProps,
parseSpotlightUsernames,
parseSpotlightType,
isDirectoryProps,
isFingerprintProps,
isMeteorCall,
Expand Down Expand Up @@ -339,7 +341,11 @@ API.v1.get(
);

const spotlightResponseSchema = ajv.compile<{
users: Pick<IUser, 'name' | 'status' | 'statusText' | 'avatarETag' | '_id' | 'username'>[];
users: (Pick<IUser, 'name' | '_id' | 'username'> &
Partial<Pick<IUser, 'status' | 'statusText' | 'avatarETag'>> & {
nickname?: string;
outside?: boolean;
})[];
rooms: Pick<IRoom, 't' | 'name' | 'lastMessage' | '_id'>[];
}>({
type: 'object',
Expand All @@ -356,7 +362,7 @@ const spotlightResponseSchema = ajv.compile<{
statusText: { type: 'string' },
avatarETag: { type: 'string' },
},
required: ['_id', 'name', 'username', 'status'],
required: ['_id', 'name', 'username'],
additionalProperties: true,
},
},
Expand All @@ -383,7 +389,10 @@ const spotlightResponseSchema = ajv.compile<{
API.v1.get(
'spotlight',
{
authRequired: true,
// DDP `spotlight` accepts anonymous calls (Accounts_AllowAnonymousRead).
// Keep parity so anonymous-user / embedded-layout flows can still
// resolve a public channel through the navbar search.
authRequired: false,
query: isSpotlightProps,
response: {
200: spotlightResponseSchema,
Expand All @@ -392,9 +401,15 @@ API.v1.get(
},
},
async function action() {
const { query } = this.queryParams;
const { query, usernames, type, rid } = this.queryParams;

const result = await spotlightMethod({ text: query, userId: this.userId });
const result = await spotlightMethod({
text: query,
userId: this.userId,
usernames: parseSpotlightUsernames(usernames),
type: parseSpotlightType(type),
rid,
});

return API.v1.success(result);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client';
import { CustomSounds } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
Expand All @@ -12,6 +14,7 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async listCustomSounds() {
methodDeprecationLogger.method('listCustomSounds', '9.0.0', '/v1/custom-sounds.list');
return CustomSounds.find({}).toArray();
},
});
3 changes: 3 additions & 0 deletions apps/meteor/app/lib/server/methods/joinRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Rooms } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
Expand All @@ -14,6 +16,7 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async joinRoom(rid, code) {
methodDeprecationLogger.method('joinRoom', '9.0.0', '/v1/channels.join');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Deprecation target is too broad for a multi-room joinRoom method.

joinRoom is used for more than public channels, but this log always points to /v1/channels.join. That replacement is not valid for all room types, so this warning can mislead integrators and create migration breakage when DDP is removed. Please gate the deprecation route by resolved room type (or avoid logging for unsupported non-channel types).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/lib/server/methods/joinRoom.ts` at line 19, The deprecation
log call methodDeprecationLogger.method('joinRoom', '9.0.0',
'/v1/channels.join') is too broad for the multi-room joinRoom method; update
joinRoom so you resolve the room type (e.g., using the existing room resolution
logic inside the joinRoom handler) and only call methodDeprecationLogger.method
with the specific channels route when the resolved type is a public channel,
otherwise either skip the deprecation log or emit a type-appropriate/generic
message; locate the deprecation call inside joinRoom and gate it behind the
room-type check (or replace with a no-op for non-channel types).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Unconditional deprecation log contradicts PR intent — joinRoom is still called via DDP for non-channel types (private groups, DMs, livechat) where channels.join does not apply. This will produce noisy deprecation warnings for legitimate DDP usage that has no REST equivalent yet. Consider either removing the log or making it conditional on room.t === 'c' after the room is fetched.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/lib/server/methods/joinRoom.ts, line 19:

<comment>Unconditional deprecation log contradicts PR intent — `joinRoom` is still called via DDP for non-channel types (private groups, DMs, livechat) where `channels.join` does not apply. This will produce noisy deprecation warnings for legitimate DDP usage that has no REST equivalent yet. Consider either removing the log or making it conditional on room.t === 'c' after the room is fetched.</comment>

<file context>
@@ -14,6 +16,7 @@ declare module '@rocket.chat/ddp-client' {
 
 Meteor.methods<ServerMethods>({
 	async joinRoom(rid, code) {
+		methodDeprecationLogger.method('joinRoom', '9.0.0', '/v1/channels.join');
 		check(rid, String);
 
</file context>

check(rid, String);

const user = await Meteor.userAsync();
Expand Down
12 changes: 7 additions & 5 deletions apps/meteor/client/hooks/useJoinRoom.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { sdk } from '../../app/utils/client/lib/SDKClient';

type UseJoinRoomMutationFunctionProps = {
rid: IRoom['_id'];
reference: string;
Expand All @@ -13,11 +11,15 @@ type UseJoinRoomMutationFunctionProps = {
export const useJoinRoom = () => {
const queryClient = useQueryClient();
const dispatchToastMessage = useToastMessageDispatch();
// TODO(ddp-removal): /v1/channels.join only resolves public channels; non-`c`
// rooms will error here (same as DDP `joinRoom` would, just via REST).
// Replace with a unified `/v1/rooms.join` (or per-type endpoints) before
// the 9.0.0 sweep removes the DDP method.
const joinChannel = useEndpoint('POST', '/v1/channels.join');

return useMutation({
mutationFn: async ({ rid, reference, type }: UseJoinRoomMutationFunctionProps) => {
await sdk.call('joinRoom', rid);

await joinChannel({ roomId: rid });
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return { reference, type };
},
onSuccess: (data) => {
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/client/lib/chats/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,10 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
const isSubscribedToRoom = async (): Promise<boolean> => !!Subscriptions.state.find((record) => record.rid === rid);

const joinRoom = async (): Promise<void> => {
await sdk.call('joinRoom', rid);
// TODO(ddp-removal): only public channels resolve through this endpoint;
// private groups, DMs and livechat used to error via DDP too — REST keeps
// that behavior. Replace with a unified `/v1/rooms.join` when available.
await sdk.rest.post('/v1/channels.join', { roomId: rid });
};

const findDiscussionByID = async (drid: IRoom['_id']): Promise<IRoom | undefined> =>
Expand Down
10 changes: 7 additions & 3 deletions apps/meteor/client/navbar/NavBarSearch/hooks/useSearchItems.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useMethod, useUserSubscriptions } from '@rocket.chat/ui-contexts';
import { useEndpoint, useUserSubscriptions } from '@rocket.chat/ui-contexts';
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useMemo } from 'react';

Expand Down Expand Up @@ -47,7 +47,7 @@ export const useSearchItems = (filterText: string): UseQueryResult<SubscriptionW
return { users: true, rooms: true, includeFederatedRooms: true };
}, [searchForChannels, searchForDMs]);

const getSpotlight = useMethod('spotlight');
const getSpotlight = useEndpoint('GET', '/v1/spotlight');

return useQuery({
queryKey: ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id, name }) => _id + name)],
Expand All @@ -57,7 +57,11 @@ export const useSearchItems = (filterText: string): UseQueryResult<SubscriptionW
return localRooms;
}

const spotlight = await getSpotlight(name, usernamesFromClient, type);
const spotlight = await getSpotlight({
query: name,
usernames: usernamesFromClient.join(','),
type: JSON.stringify(type),
});

const filterUsersUnique = ({ _id }: { _id: string }, index: number, arr: { _id: string }[]): boolean =>
index === arr.findIndex((user) => _id === user._id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { ICustomSound } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { CustomSoundContext, useStream, useUserPreference } from '@rocket.chat/ui-contexts';
import { CustomSoundContext, useEndpoint, useStream, useUserPreference } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, type ReactNode } from 'react';

import { defaultSounds, getCustomSoundURL, formatVolume } from './lib';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { useUserSoundPreferences } from '../../hooks/useUserSoundPreferences';

type CustomSoundProviderProps = {
Expand All @@ -17,18 +16,32 @@ const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => {

const queryClient = useQueryClient();
const streamAll = useStream('notify-all');
const getCustomSounds = useEndpoint('GET', '/v1/custom-sounds.list');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the old listCustomSounds used to return ALL custom sounds (with no pagination) but /v1/custom-sounds.list is paginated, so it won't load all sound to the provider


const newRoomNotification = useUserPreference<string>('newRoomNotification') || 'door';
const newMessageNotification = useUserPreference<string>('newMessageNotification') || 'chime';
const { notificationsSoundVolume, voipRingerVolume } = useUserSoundPreferences();

const { data: list } = useQuery({
queryFn: async (): Promise<Omit<ICustomSound, '_updatedAt'>[]> => {
const customSoundsList = await sdk.call('listCustomSounds');
if (!customSoundsList.length) {
// `/v1/custom-sounds.list` is paginated, so we page through all results to
// load every custom sound into the provider (the legacy `listCustomSounds`
// method returned them all at once).
const sounds: Awaited<ReturnType<typeof getCustomSounds>>['sounds'] = [];
let total = Infinity;
while (sounds.length < total) {
const page = await getCustomSounds({ count: 100, offset: sounds.length });
total = page.total;
sounds.push(...page.sounds);
if (!page.sounds.length) {
break;
}
}

if (!sounds.length) {
return defaultSounds;
}
return [...customSoundsList.map((sound) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds];
return [...sounds.map(({ _updatedAt: _, ...sound }) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds];
},
queryKey: ['listCustomSounds'],
initialData: defaultSounds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getURL } from '../../../../app/utils/client';

export const getAssetUrl = (asset: string, params?: Record<string, any>) => getURL(asset, params, undefined, true);

export const getCustomSoundURL = (sound: ICustomSound) => {
export const getCustomSoundURL = (sound: Pick<ICustomSound, '_id' | 'extension'> & Pick<Partial<ICustomSound>, 'random'>) => {
return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 });
};

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/startup/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if (!sdkTransportEnabled) {

const utcOffset = -new Date().getTimezoneOffset() / 60;
if (user.utcOffset !== utcOffset) {
sdk.call('userSetUtcOffset', utcOffset);
void sdk.rest.post('/v1/users.setPreferences', { data: { utcOffset } });
}

emitStatusChange(user.status);
Expand Down Expand Up @@ -79,7 +79,7 @@ if (!sdkTransportEnabled) {

const utcOffset = -new Date().getTimezoneOffset() / 60;
if (user.utcOffset !== utcOffset) {
sdk.call('userSetUtcOffset', utcOffset);
void sdk.rest.post('/v1/users.setPreferences', { data: { utcOffset } });
}

emitStatusChange(user.status);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { IUpload } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';

export const useDeleteFile = (reload: () => void) => {
const { t } = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const deleteFile = useMethod('deleteFileMessage');
const deleteMessage = useEndpoint('POST', '/v1/chat.delete');

const handleDelete = useStableCallback((_id: IUpload['_id']) => {
const onConfirm = async () => {
try {
await deleteFile(_id);
await deleteMessage({ fileId: _id });
dispatchToastMessage({ type: 'success', message: t('Deleted') });
reload();
} catch (error) {
Expand Down
Loading
Loading