-
Notifications
You must be signed in to change notification settings - Fork 13.7k
feat: extend 4 REST endpoints + migrate 7 DDP callers #40711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d63aeb2
58e938e
a9fdd40
96f1329
1e11291
068b252
b6e8b25
22e528b
362e0ae
4d71f24
daa64d6
5c78031
46a920e
797dae4
d12ff77
e26ed03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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` |
| 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. |
| 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. |
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' }, | ||
|
|
@@ -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 } }); | ||
|
|
||
| if (!msg) { | ||
| if ('fileId' in this.bodyParams) { | ||
| return API.v1.failure(`No message found with the file id: "${this.bodyParams.fileId}".`); | ||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: Missing Authorization Check in chat.starMessage/unStarMessage The Tracegraph 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
Fix with AITriage: Reply |
||
| return API.v1.failure('The room id provided does not match where the message is from.'); | ||
| } | ||
|
|
||
|
|
@@ -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 }, | ||
| }); | ||
| }, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deprecation target is too broad for a multi-room
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Unconditional deprecation log contradicts PR intent — Prompt for AI agents |
||
| check(rid, String); | ||
|
|
||
| const user = await Meteor.userAsync(); | ||
|
|
||
| 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 = { | ||
|
|
@@ -17,18 +16,32 @@ const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { | |
|
|
||
| const queryClient = useQueryClient(); | ||
| const streamAll = useStream('notify-all'); | ||
| const getCustomSounds = useEndpoint('GET', '/v1/custom-sounds.list'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the old |
||
|
|
||
| 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, | ||
|
|
||
There was a problem hiding this comment.
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
fileIdandmsgIdto avoid ambiguous delete-target resolution.Prompt for AI agents