Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/chat-getmessagereadreceipts-pagination-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': patch
'@rocket.chat/meteor': patch
---

Fix `chat.getMessageReadReceipts` endpoint schema validation failing when `offset` and `count` pagination query parameters are passed.
6 changes: 4 additions & 2 deletions apps/meteor/ee/server/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { getReadReceiptsFunction } from '../meteor-methods/getReadReceipts';

type GetMessageReadReceiptsProps = {
messageId: IMessage['_id'];
count?: number;
offset?: number;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

declare module '@rocket.chat/rest-typings' {
Expand Down Expand Up @@ -52,10 +54,10 @@ API.v1.get(
throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature');
}

const { messageId } = this.queryParams;
const { messageId, offset, count } = this.queryParams;

return API.v1.success({
receipts: await getReadReceiptsFunction(messageId, this.userId),
receipts: await getReadReceiptsFunction(messageId, this.userId, { offset, count }),
});
},
);
13 changes: 11 additions & 2 deletions apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ class ReadReceiptClass {
}
}

async getReceipts(message: Pick<IMessage, '_id' | 'receiptsArchived'>): Promise<IReadReceiptWithUser[]> {
async getReceipts(
message: Pick<IMessage, '_id' | 'receiptsArchived'>,
options?: { offset?: number; count?: number },
): Promise<IReadReceiptWithUser[]> {
// Query hot storage (always)
const hotReceipts = await ReadReceipts.findByMessageId(message._id).toArray();

Expand All @@ -148,7 +151,13 @@ class ReadReceiptClass {
}

// Combine receipts from both storages
const receipts = [...new Map([...hotReceipts, ...coldReceipts].map((receipt) => [receipt._id, receipt])).values()];
let receipts = [...new Map([...hotReceipts, ...coldReceipts].map((receipt) => [receipt._id, receipt])).values()];

if (options?.offset !== undefined || options?.count !== undefined) {
const offset = options.offset ?? 0;
const count = options.count ?? receipts.length;
receipts = receipts.slice(offset, offset + count);
}

// get unique receipts user ids
const userIds = [...new Set(receipts.map((receipt) => receipt.userId))];
Expand Down
8 changes: 6 additions & 2 deletions apps/meteor/ee/server/meteor-methods/getReadReceipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ declare module '@rocket.chat/ddp-client' {
}
}

export const getReadReceiptsFunction = async function (messageId: IMessage['_id'], userId: string): Promise<IReadReceiptWithUser[]> {
export const getReadReceiptsFunction = async function (
messageId: IMessage['_id'],
userId: string,
options?: { offset?: number; count?: number },
): Promise<IReadReceiptWithUser[]> {
if (!License.hasModule('message-read-receipt')) {
throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' });
}
Expand All @@ -33,7 +37,7 @@ export const getReadReceiptsFunction = async function (messageId: IMessage['_id'
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getReadReceipts' });
}

return ReadReceipt.getReceipts(message);
return ReadReceipt.getReceipts(message, options);
};

Meteor.methods<ServerMethods>({
Expand Down
23 changes: 23 additions & 0 deletions apps/meteor/tests/end-to-end/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2990,6 +2990,29 @@ describe('[Chat]', () => {
})
.end(done);
});

it('should return statusCode: 200 and apply offset and count pagination parameters', function (done) {
if (!isEnterprise) {
this.skip();
}

void request
.get(api(`chat.getMessageReadReceipts`))
.set(credentials)
.query({
messageId: message._id,
offset: 0,
count: 1,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('receipts').and.to.be.an('array');

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: This test only asserts HTTP 200 and that receipts is an array, so it would pass even if the server ignored offset/count entirely — which it currently does (the endpoint action destructures only messageId and calls getReadReceiptsFunction(messageId, this.userId) with no pagination applied). It therefore provides no coverage of the pagination behavior this PR claims to add. Consider verifying actual pagination, e.g. assert that a request with count returns at most count receipts and that offset shifts the returned window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/tests/end-to-end/api/chat.ts, line 3010:

<comment>This test only asserts HTTP 200 and that `receipts` is an array, so it would pass even if the server ignored `offset`/`count` entirely — which it currently does (the endpoint action destructures only `messageId` and calls `getReadReceiptsFunction(messageId, this.userId)` with no pagination applied). It therefore provides no coverage of the pagination behavior this PR claims to add. Consider verifying actual pagination, e.g. assert that a request with `count` returns at most `count` receipts and that `offset` shifts the returned window.</comment>

<file context>
@@ -2990,6 +2990,28 @@ describe('[Chat]', () => {
+					.expect('Content-Type', 'application/json')
+					.expect(200)
+					.expect((res) => {
+						expect(res.body).to.have.property('receipts').and.to.be.an('array');
+						expect(res.body).to.have.property('success', true);
+					})
</file context>

expect(res.body.receipts.length).to.be.at.most(1);
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});

describe('when an error occurs', () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@ export const isChatUpdateProps = ajv.compile<ChatUpdate>(ChatUpdateSchema);

type ChatGetMessageReadReceipts = {
messageId: IMessage['_id'];
count?: number;
offset?: number;
};

const ChatGetMessageReadReceiptsSchema = {
Expand All @@ -497,12 +499,20 @@ const ChatGetMessageReadReceiptsSchema = {
messageId: {
type: 'string',
},
offset: {
type: 'integer',
minimum: 0,
},
count: {
type: 'integer',
minimum: 0,
},
},
required: ['messageId'],
additionalProperties: false,
};

export const isChatGetMessageReadReceiptsProps = ajv.compile<ChatGetMessageReadReceipts>(ChatGetMessageReadReceiptsSchema);
export const isChatGetMessageReadReceiptsProps = ajvQuery.compile<ChatGetMessageReadReceipts>(ChatGetMessageReadReceiptsSchema);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

type GetStarredMessages = {
roomId: IRoom['_id'];
Expand Down