Skip to content
This repository was archived by the owner on Nov 5, 2025. It is now read-only.

Commit aec2401

Browse files
Dnouvd-gubert
andauthored
feat: implement room reader method to get messages in a room (#770)
Co-authored-by: Douglas Gubert <[email protected]>
1 parent b5dcb6d commit aec2401

File tree

9 files changed

+183
-47
lines changed

9 files changed

+183
-47
lines changed

src/definition/accessors/IRoomRead.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { IMessage } from '../messages/index';
1+
import type { GetMessagesOptions } from '../../server/bridges/RoomBridge';
2+
import type { IMessageRaw } from '../messages/index';
23
import type { IRoom } from '../rooms/index';
34
import type { IUser } from '../users/index';
45

@@ -40,12 +41,16 @@ export interface IRoomRead {
4041
getCreatorUserByName(name: string): Promise<IUser | undefined>;
4142

4243
/**
43-
* Gets an iterator for all of the messages in the provided room.
44+
* Retrieves an array of messages from the specified room.
4445
*
45-
* @param roomId the room's id
46-
* @returns an iterator for messages
46+
* @param roomId The unique identifier of the room from which to retrieve messages.
47+
* @param options Optional parameters for retrieving messages:
48+
* - limit: The maximum number of messages to retrieve. Maximum 100
49+
* - skip: The number of messages to skip (for pagination).
50+
* - sort: An object defining the sorting order of the messages. Each key is a field to sort by, and the value is either "asc" for ascending order or "desc" for descending order.
51+
* @returns A Promise that resolves to an array of IMessage objects representing the messages in the room.
4752
*/
48-
getMessages(roomId: string): Promise<IterableIterator<IMessage>>;
53+
getMessages(roomId: string, options?: Partial<GetMessagesOptions>): Promise<Array<IMessageRaw>>;
4954

5055
/**
5156
* Gets an iterator for all of the users in the provided room.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { IBlock, Block } from '@rocket.chat/ui-kit';
2+
3+
import type { IRoom } from '../rooms';
4+
import type { IUserLookup } from '../users';
5+
import type { IMessageAttachment } from './IMessageAttachment';
6+
import type { IMessageFile } from './IMessageFile';
7+
import type { IMessageReactions } from './IMessageReaction';
8+
9+
/**
10+
* The raw version of a message, without resolved information for relationship fields, i.e.
11+
* `room`, `sender` and `editor` are not the complete entity like they are in `IMessage`
12+
*
13+
* This is used in methods that fetch multiple messages at the same time, as resolving the relationship
14+
* fields require additional queries to the database and would hit the system's performance significantly.
15+
*/
16+
export interface IMessageRaw {
17+
id: string;
18+
roomId: IRoom['id'];
19+
sender: IUserLookup;
20+
createdAt: Date;
21+
threadId?: string;
22+
text?: string;
23+
updatedAt?: Date;
24+
editor?: IUserLookup;
25+
editedAt?: Date;
26+
emoji?: string;
27+
avatarUrl?: string;
28+
alias?: string;
29+
file?: IMessageFile;
30+
attachments?: Array<IMessageAttachment>;
31+
reactions?: IMessageReactions;
32+
groupable?: boolean;
33+
parseUrls?: boolean;
34+
customFields?: { [key: string]: any };
35+
blocks?: Array<IBlock | Block>;
36+
starred?: Array<{ _id: string }>;
37+
pinned?: boolean;
38+
pinnedAt?: Date;
39+
pinnedBy?: IUserLookup;
40+
}

src/definition/messages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IMessageDeleteContext } from './IMessageDeleteContext';
88
import { IMessageFile } from './IMessageFile';
99
import { IMessageFollowContext } from './IMessageFollowContext';
1010
import { IMessagePinContext } from './IMessagePinContext';
11+
import { IMessageRaw } from './IMessageRaw';
1112
import { IMessageReaction, IMessageReactions } from './IMessageReaction';
1213
import { IMessageReactionContext } from './IMessageReactionContext';
1314
import { IMessageReportContext } from './IMessageReportContext';
@@ -39,6 +40,7 @@ export {
3940
IMessageAttachmentField,
4041
IMessageAction,
4142
IMessageFile,
43+
IMessageRaw,
4244
IMessageReactions,
4345
IMessageReaction,
4446
IPostMessageDeleted,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface IUserLookup {
22
_id: string;
33
username: string;
4+
name?: string;
45
}

src/server/accessors/RoomRead.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { IRoomRead } from '../../definition/accessors';
2-
import type { IMessage } from '../../definition/messages';
2+
import type { IMessageRaw } from '../../definition/messages';
33
import type { IRoom } from '../../definition/rooms';
44
import type { IUser } from '../../definition/users';
55
import type { RoomBridge } from '../bridges';
6+
import { type GetMessagesOptions, GetMessagesSortableFields } from '../bridges/RoomBridge';
67

78
export class RoomRead implements IRoomRead {
89
constructor(private roomBridge: RoomBridge, private appId: string) {}
@@ -23,8 +24,18 @@ export class RoomRead implements IRoomRead {
2324
return this.roomBridge.doGetCreatorByName(name, this.appId);
2425
}
2526

26-
public getMessages(roomId: string): Promise<IterableIterator<IMessage>> {
27-
throw new Error('Method not implemented.');
27+
public getMessages(roomId: string, options: Partial<GetMessagesOptions> = {}): Promise<IMessageRaw[]> {
28+
if (typeof options.limit !== 'undefined' && (!Number.isFinite(options.limit) || options.limit > 100)) {
29+
throw new Error(`Invalid limit provided. Expected number <= 100, got ${options.limit}`);
30+
}
31+
32+
options.limit ??= 100;
33+
34+
if (options.sort) {
35+
this.validateSort(options.sort);
36+
}
37+
38+
return this.roomBridge.doGetMessages(roomId, options as GetMessagesOptions, this.appId);
2839
}
2940

3041
public getMembers(roomId: string): Promise<Array<IUser>> {
@@ -46,4 +57,17 @@ export class RoomRead implements IRoomRead {
4657
public getLeaders(roomId: string): Promise<Array<IUser>> {
4758
return this.roomBridge.doGetLeaders(roomId, this.appId);
4859
}
60+
61+
// If there are any invalid fields or values, throw
62+
private validateSort(sort: Record<string, unknown>) {
63+
Object.entries(sort).forEach(([key, value]) => {
64+
if (!GetMessagesSortableFields.includes(key as typeof GetMessagesSortableFields[number])) {
65+
throw new Error(`Invalid key "${key}" used in sort. Available keys for sorting are ${GetMessagesSortableFields.join(', ')}`);
66+
}
67+
68+
if (value !== 'asc' && value !== 'desc') {
69+
throw new Error(`Invalid sort direction for field "${key}". Expected "asc" or "desc", got ${value}`);
70+
}
71+
});
72+
}
4973
}

src/server/bridges/RoomBridge.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import type { IMessage } from '../../definition/messages';
1+
import type { IMessage, IMessageRaw } from '../../definition/messages';
22
import type { IRoom } from '../../definition/rooms';
33
import type { IUser } from '../../definition/users';
44
import { PermissionDeniedError } from '../errors/PermissionDeniedError';
55
import { AppPermissionManager } from '../managers/AppPermissionManager';
66
import { AppPermissions } from '../permissions/AppPermissions';
77
import { BaseBridge } from './BaseBridge';
88

9+
export const GetMessagesSortableFields = ['createdAt'] as const;
10+
11+
export type GetMessagesOptions = {
12+
limit: number;
13+
skip: number;
14+
sort: Record<typeof GetMessagesSortableFields[number], 'asc' | 'desc'>;
15+
};
16+
917
export abstract class RoomBridge extends BaseBridge {
1018
public async doCreate(room: IRoom, members: Array<string>, appId: string): Promise<string> {
1119
if (this.hasWritePermission(appId)) {
@@ -91,6 +99,12 @@ export abstract class RoomBridge extends BaseBridge {
9199
}
92100
}
93101

102+
public async doGetMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]> {
103+
if (this.hasReadPermission(appId)) {
104+
return this.getMessages(roomId, options, appId);
105+
}
106+
}
107+
94108
public async doRemoveUsers(roomId: string, usernames: Array<string>, appId: string): Promise<void> {
95109
if (this.hasWritePermission(appId)) {
96110
return this.removeUsers(roomId, usernames, appId);
@@ -129,6 +143,8 @@ export abstract class RoomBridge extends BaseBridge {
129143

130144
protected abstract getLeaders(roomId: string, appId: string): Promise<Array<IUser>>;
131145

146+
protected abstract getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]>;
147+
132148
protected abstract removeUsers(roomId: string, usernames: Array<string>, appId: string): Promise<void>;
133149

134150
private hasWritePermission(appId: string): boolean {

tests/server/accessors/RoomRead.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ import type { IUser } from '../../../src/definition/users';
55
import { RoomRead } from '../../../src/server/accessors';
66
import type { RoomBridge } from '../../../src/server/bridges';
77
import { TestData } from '../../test-data/utilities';
8+
import type { IMessageRaw } from '../../../src/definition/messages';
89

910
export class RoomReadAccessorTestFixture {
1011
private room: IRoom;
1112

1213
private user: IUser;
1314

15+
private messages: IMessageRaw[];
16+
1417
private mockRoomBridgeWithRoom: RoomBridge;
1518

1619
@SetupFixture
1720
public setupFixture() {
1821
this.room = TestData.getRoom();
1922
this.user = TestData.getUser();
23+
this.messages = ['507f1f77bcf86cd799439011', '507f191e810c19729de860ea'].map((id) => TestData.getMessageRaw(id));
2024

2125
const theRoom = this.room;
2226
const theUser = this.user;
27+
const theMessages = this.messages;
2328
this.mockRoomBridgeWithRoom = {
2429
doGetById(id, appId): Promise<IRoom> {
2530
return Promise.resolve(theRoom);
@@ -39,6 +44,9 @@ export class RoomReadAccessorTestFixture {
3944
doGetMembers(name, appId): Promise<Array<IUser>> {
4045
return Promise.resolve([theUser]);
4146
},
47+
doGetMessages(roomId, options, appId): Promise<IMessageRaw[]> {
48+
return Promise.resolve(theMessages);
49+
},
4250
} as RoomBridge;
4351
}
4452

@@ -58,14 +66,15 @@ export class RoomReadAccessorTestFixture {
5866
Expect(await rr.getCreatorUserByName('testing')).toBe(this.user);
5967
Expect(await rr.getDirectByUsernames([this.user.username])).toBeDefined();
6068
Expect(await rr.getDirectByUsernames([this.user.username])).toBe(this.room);
69+
Expect(await rr.getMessages('testing')).toBeDefined();
70+
Expect(await rr.getMessages('testing')).toBe(this.messages);
6171
}
6272

6373
@AsyncTest()
6474
public async useTheIterators() {
6575
Expect(() => new RoomRead(this.mockRoomBridgeWithRoom, 'testing-app')).not.toThrow();
6676

6777
const rr = new RoomRead(this.mockRoomBridgeWithRoom, 'testing-app');
68-
await Expect(() => rr.getMessages('faker')).toThrowErrorAsync(Error, 'Method not implemented.');
6978

7079
Expect(await rr.getMembers('testing')).toBeDefined();
7180
Expect((await rr.getMembers('testing')) as Array<IUser>).not.toBeEmpty();

tests/test-data/bridges/roomBridge.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type { IMessage } from '../../../src/definition/messages';
1+
import type { IMessage, IMessageRaw } from '../../../src/definition/messages';
22
import type { IRoom } from '../../../src/definition/rooms';
33
import type { IUser } from '../../../src/definition/users';
44
import { RoomBridge } from '../../../src/server/bridges';
5+
import type { GetMessagesOptions } from '../../../src/server/bridges/RoomBridge';
56

67
export class TestsRoomBridge extends RoomBridge {
78
public create(room: IRoom, members: Array<string>, appId: string): Promise<string> {
@@ -32,6 +33,10 @@ export class TestsRoomBridge extends RoomBridge {
3233
throw new Error('Method not implemented.');
3334
}
3435

36+
public getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]> {
37+
throw new Error('Method not implemented.');
38+
}
39+
3540
public update(room: IRoom, members: Array<string>, appId: string): Promise<void> {
3641
throw new Error('Method not implemented.');
3742
}

tests/test-data/utilities.ts

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { IHttp, IModify, IPersistence, IRead } from '../../src/definition/accessors';
22
import { HttpStatusCode } from '../../src/definition/accessors';
3-
import type { IMessage } from '../../src/definition/messages';
3+
import type { IMessage, IMessageAttachment, IMessageRaw } from '../../src/definition/messages';
44
import type { IRoom } from '../../src/definition/rooms';
55
import { RoomType } from '../../src/definition/rooms';
66
import type { ISetting } from '../../src/definition/settings';
@@ -128,6 +128,39 @@ export class TestInfastructureSetup {
128128
}
129129

130130
const date = new Date();
131+
132+
const DEFAULT_ATTACHMENT = {
133+
color: '#00b2b2',
134+
collapsed: false,
135+
text: 'Just an attachment that is used for testing',
136+
timestampLink: 'https://google.com/',
137+
thumbnailUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
138+
author: {
139+
name: 'Author Name',
140+
link: 'https://github.com/graywolf336',
141+
icon: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
142+
},
143+
title: {
144+
value: 'Attachment Title',
145+
link: 'https://github.com/RocketChat',
146+
displayDownloadLink: false,
147+
},
148+
imageUrl: 'https://rocket.chat/images/default/logo.svg',
149+
audioUrl: 'http://www.w3schools.com/tags/horse.mp3',
150+
videoUrl: 'http://www.w3schools.com/tags/movie.mp4',
151+
fields: [
152+
{
153+
short: true,
154+
title: 'Test',
155+
value: 'Testing out something or other',
156+
},
157+
{
158+
short: true,
159+
title: 'Another Test',
160+
value: '[Link](https://google.com/) something and this and that.',
161+
},
162+
],
163+
};
131164
export class TestData {
132165
public static getDate(): Date {
133166
return date;
@@ -193,41 +226,42 @@ export class TestData {
193226
emoji: ':see_no_evil:',
194227
avatarUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
195228
alias: 'Testing Bot',
196-
attachments: [
197-
{
198-
collapsed: false,
199-
color: '#00b2b2',
200-
text: 'Just an attachment that is used for testing',
201-
timestamp: new Date(),
202-
timestampLink: 'https://google.com/',
203-
thumbnailUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
204-
author: {
205-
name: 'Author Name',
206-
link: 'https://github.com/graywolf336',
207-
icon: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
208-
},
209-
title: {
210-
value: 'Attachment Title',
211-
link: 'https://github.com/RocketChat',
212-
displayDownloadLink: false,
213-
},
214-
imageUrl: 'https://rocket.chat/images/default/logo.svg',
215-
audioUrl: 'http://www.w3schools.com/tags/horse.mp3',
216-
videoUrl: 'http://www.w3schools.com/tags/movie.mp4',
217-
fields: [
218-
{
219-
short: true,
220-
title: 'Test',
221-
value: 'Testing out something or other',
222-
},
223-
{
224-
short: true,
225-
title: 'Another Test',
226-
value: '[Link](https://google.com/) something and this and that.',
227-
},
228-
],
229-
},
230-
],
229+
attachments: [this.createAttachment()],
230+
};
231+
}
232+
233+
public static getMessageRaw(id?: string, text?: string): IMessageRaw {
234+
const editorUser = TestData.getUser();
235+
const senderUser = TestData.getUser();
236+
237+
return {
238+
id: id || '4bShvoOXqB',
239+
roomId: TestData.getRoom().id,
240+
sender: {
241+
_id: senderUser.id,
242+
username: senderUser.username,
243+
name: senderUser?.name,
244+
},
245+
text: text || 'This is just a test, do not be alarmed',
246+
createdAt: date,
247+
updatedAt: new Date(),
248+
editor: {
249+
_id: editorUser.id,
250+
username: editorUser.username,
251+
},
252+
editedAt: new Date(),
253+
emoji: ':see_no_evil:',
254+
avatarUrl: 'https://avatars0.githubusercontent.com/u/850391?s=88&v=4',
255+
alias: 'Testing Bot',
256+
attachments: [this.createAttachment()],
257+
};
258+
}
259+
260+
private static createAttachment(attachment?: IMessageAttachment): IMessageAttachment {
261+
attachment = attachment || DEFAULT_ATTACHMENT;
262+
return {
263+
timestamp: new Date(),
264+
...attachment,
231265
};
232266
}
233267

0 commit comments

Comments
 (0)