Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/nine-news-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Migrate groups.history with AJV validation and schema types
199 changes: 141 additions & 58 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ import { isTruthy } from '@rocket.chat/tools';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { Filter } from 'mongodb';

import {IMessage} from '@rocket.chat/core-typings'
import {
ajv,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
PaginatedRequest,
withGroupBaseProperties,
GroupsBaseProps,

} from '@rocket.chat/rest-typings';

import type { ExtractRoutesFromAPI } from '../ApiClass';
import { eraseRoom } from '../../../../server/lib/eraseRoom';
import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
import { openRoom } from '../../../../server/lib/openRoom';
Expand Down Expand Up @@ -37,6 +49,126 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag
import { getPaginationItems } from '../helpers/getPaginationItems';
import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams';

type GroupsHistoryProps = PaginatedRequest<
GroupsBaseProps & {
latest?: string;
oldest?: string;
inclusive?: 'true' | 'false';
unreads?: 'true' | 'false';
showThreadMessages?: string;
}
>;
const groupsHistoryPropsSchema = withGroupBaseProperties({
latest: {
type: 'string',
},
oldest: {
type: 'string',
},
inclusive: {
type: 'string',
},
unreads: {
type: 'string',
},
showThreadMessages: {
type: 'string',
},
count: {
type: 'string',
},
offset: {
type: 'string',
},
sort: {
type: 'string',
},
});
const isGroupsHistoryProps = ajv.compile<GroupsHistoryProps>(groupsHistoryPropsSchema);

const isGroupsHistoryReponse = ajv.compile({
type: 'object',
properties: {
success: { type: 'boolean', enum: [true] },
messages: {
type: 'array',
items: { $ref: '#/components/schemas/IMessage' },
},
count: { type: 'integer' },
offset: { type: 'integer' },
total: { type: 'integer' },
unreadNotLoaded: { type: 'integer' },
firstUnread: { $ref: '#/components/schemas/IMessage' },
},
required: ['success', 'messages'],
additionalProperties: false,
});

const groupsHistoryEndpoints = API.v1.get(
'groups.history',
{
authRequired: true,
query: isGroupsHistoryProps,
response: {
200: isGroupsHistoryReponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const findResult = await findPrivateGroupByIdOrName({
params: this.queryParams,
userId: this.userId,
checkedArchived: false,
});

let latestDate = new Date();
if (this.queryParams.latest) {
latestDate = new Date(this.queryParams.latest);
}

let oldestDate = undefined;
if (this.queryParams.oldest) {
oldestDate = new Date(this.queryParams.oldest);
}

const inclusive = this.queryParams.inclusive === 'true';

let count = 20;
if (this.queryParams.count) {
count = parseInt(String(this.queryParams.count));
}

let offset = 0;
if (this.queryParams.offset) {
offset = parseInt(String(this.queryParams.offset));
}

const unreads = this.queryParams.unreads === 'true';
const showThreadMessages = this.queryParams.showThreadMessages !== 'false';

const result = await getChannelHistory({
rid: findResult.rid,
fromUserId: this.userId,
latest: latestDate,
oldest: oldestDate,
inclusive,
offset,
count,
unreads,
showThreadMessages,
});

if (!result) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

return API.v1.success(result as { messages: IMessage[]; firstUnread?: IMessage; unreadNotLoaded?: number });
},
);


async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise<IRoom> {
if (
(!('roomId' in params) && !('roomName' in params)) ||
Expand Down Expand Up @@ -488,63 +620,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'groups.history',
{ authRequired: true },
{
async get() {
const findResult = await findPrivateGroupByIdOrName({
params: this.queryParams,
userId: this.userId,
checkedArchived: false,
});

let latestDate = new Date();
if (this.queryParams.latest) {
latestDate = new Date(this.queryParams.latest);
}

let oldestDate = undefined;
if (this.queryParams.oldest) {
oldestDate = new Date(this.queryParams.oldest);
}

const inclusive = this.queryParams.inclusive === 'true';

let count = 20;
if (this.queryParams.count) {
count = parseInt(String(this.queryParams.count));
}

let offset = 0;
if (this.queryParams.offset) {
offset = parseInt(String(this.queryParams.offset));
}

const unreads = this.queryParams.unreads === 'true';

const showThreadMessages = this.queryParams.showThreadMessages !== 'false';

const result = await getChannelHistory({
rid: findResult.rid,
fromUserId: this.userId,
latest: latestDate,
oldest: oldestDate,
inclusive,
offset,
count,
unreads,
showThreadMessages,
});

if (!result) {
return API.v1.forbidden();
}

return API.v1.success(result);
},
},
);

API.v1.addRoute(
'groups.info',
Expand Down Expand Up @@ -1300,3 +1375,11 @@ API.v1.addRoute(
},
},
);


export type GroupsHistoryEndpoints = ExtractRoutesFromAPI<typeof groupsHistoryEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends GroupsHistoryEndpoints {}
}
4 changes: 2 additions & 2 deletions packages/rest-typings/src/helpers/PaginatedRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type PaginatedRequest<T = Record<string, boolean | number | string | object>, S extends string = string> = {
count?: number;
offset?: number;
count?: string;
offset?: string;
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.

avoid changing unrelated code within the API. I suspect running the tests and eslint would have flagged these issues. I recommend reverting these strings to numbers and running yarn testapi and yarn eslint:fix inside the mentor folder

Copy link
Copy Markdown
Author

@Harxhit Harxhit Mar 24, 2026

Choose a reason for hiding this comment

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

Sir, I have reverted the changes as requested. However, the issue is that the existing test cases send count and offset as numbers, while HTTP converts them into strings because of that one test case is failing. If you prefer, I can update the schema to accept both strings and integers.

sort?: `{ "${S}": ${1 | -1} }` | string;
/* deprecated */
query?: string;
Expand Down
49 changes: 0 additions & 49 deletions packages/rest-typings/src/v1/groups/GroupsHistoryProps.ts

This file was deleted.

6 changes: 0 additions & 6 deletions packages/rest-typings/src/v1/groups/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { GroupsCreateProps } from './GroupsCreateProps';
import type { GroupsDeleteProps } from './GroupsDeleteProps';
import type { GroupsFilesProps } from './GroupsFilesProps';
import type { GroupsGetIntegrationsProps } from './GroupsGetIntegrationsProps';
import type { GroupsHistoryProps } from './GroupsHistoryProps';
import type { GroupsInfoProps } from './GroupsInfoProps';
import type { GroupsInviteProps } from './GroupsInviteProps';
import type { GroupsKickProps } from './GroupsKickProps';
Expand Down Expand Up @@ -54,11 +53,6 @@ export type GroupsEndpoints = {
total: number;
};
};
'/v1/groups.history': {
GET: (params: GroupsHistoryProps) => PaginatedResult<{
messages: IMessage[];
}>;
};
'/v1/groups.archive': {
POST: (params: GroupsArchiveProps) => void;
};
Expand Down
1 change: 1 addition & 0 deletions packages/rest-typings/src/v1/groups/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type * from './groups';

export * from './BaseProps';
export * from './GroupsArchiveProps';
export * from './GroupsCloseProps';
export * from './GroupsConvertToTeamProps';
Expand Down