Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b21607a
Introduce bridge to read rooms
Dnouv Dec 8, 2025
8224b9c
Limit 100 rooms, use lightweight room converter
Dnouv Dec 8, 2025
7f2531e
Add changeset
Dnouv Dec 8, 2025
ef18f11
Introduce discussion and team type
Dnouv Dec 11, 2025
7ed4173
use roomraw type
Dnouv Dec 11, 2025
7ff350d
Fix discussion filter
Dnouv Dec 11, 2025
f9589a7
Avoid repeated checks
Dnouv Dec 11, 2025
f847149
Remove duplicate imports
Dnouv Dec 11, 2025
851fc46
Correct error message
Dnouv Dec 11, 2025
aa4c18c
Make the types consistent
Dnouv Dec 11, 2025
0d421c0
Remove unused convertRoomWithoutLookups
Dnouv Dec 12, 2025
ea626e0
refactor: Copilot nitpick
d-gubert Dec 12, 2025
81c52b4
Update .changeset/chatty-dingos-bathe.md
Dnouv Dec 12, 2025
139d265
handle reviews \- Remove DISCUSSION and TEAM from RoomType (they’re n…
Dnouv Dec 12, 2025
9471487
Introduce teamId and teamMain
Dnouv Dec 12, 2025
26bd440
Add more fields, buildRoomQuery was refactored so filtering discussio…
Dnouv Dec 15, 2025
448a726
Handle nit for doc update
Dnouv Dec 15, 2025
0d81c52
optimize code
Dnouv Dec 15, 2025
4687c38
Remove type casting
Dnouv Dec 15, 2025
d9eb3e1
remove cache field _USERNAMES
Dnouv Dec 15, 2025
6606e7e
Fixed the type filter leak: when onlyDiscussions or onlyTeamMain is s…
Dnouv Dec 15, 2025
5694531
Delete mapped fields
Dnouv Dec 15, 2025
611cb98
fix: convertRoomRaw should not be async
d-gubert Dec 15, 2025
4ad6ebd
Add projections following adminFields
Dnouv Dec 15, 2025
3b89b28
Revert "fix: convertRoomRaw should not be async"
d-gubert Dec 15, 2025
00b9449
test: Add comprehensive edge case validation tests for getAllRooms (#…
Copilot Dec 15, 2025
59674fb
Handle v._id
Dnouv Dec 15, 2025
500aa0a
Remove duplicate test and introduce new params tests
Dnouv Dec 15, 2025
6322662
Delete u
Dnouv Dec 15, 2025
60318fe
Merge branch 'develop' into new/ae/list_rooms
d-gubert Dec 16, 2025
849757e
Add view all permissions
Dnouv Dec 16, 2025
118f795
Use a dedicated rooms model method instead of using find directly;
Dnouv Dec 17, 2025
74bbe9a
Merge branch 'develop' into new/ae/list_rooms
d-gubert Dec 17, 2025
43a9612
refactor: make new method more similar to existing query
d-gubert Dec 17, 2025
f8614cc
refactor: apps API to better interact with model method
d-gubert Dec 17, 2025
f578f63
test: fix unit tests
d-gubert Dec 17, 2025
491864e
Merge remote-tracking branch 'origin' into new/ae/list_rooms
d-gubert Dec 18, 2025
6104365
Apply suggestion from @ggazzo
d-gubert Dec 18, 2025
3ac453f
feat: add index to cover room teamMain field
d-gubert Dec 18, 2025
a52324a
refactor: simplify query in new method
d-gubert Dec 18, 2025
3a6df16
feat: increase visitor entity parity
d-gubert Dec 19, 2025
a6f3877
fix: room raw types
d-gubert Dec 19, 2025
832a1a5
fix: properly convert visitor property
d-gubert Dec 19, 2025
ab7112d
Apply suggestions from code review
d-gubert Dec 19, 2025
40ea800
fix: room bridge fixture
d-gubert Dec 19, 2025
1558452
Merge branch 'develop' into new/ae/list_rooms
d-gubert Dec 19, 2025
b6bac7f
change 'filter' parameter to 'filters'
Dnouv Dec 19, 2025
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
7 changes: 7 additions & 0 deletions .changeset/chatty-dingos-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/apps-engine': minor
'@rocket.chat/apps': minor
'@rocket.chat/meteor': minor
---

Adds a new method to the Apps-Engine that allows apps to retrieve multiple rooms from database
70 changes: 68 additions & 2 deletions apps/meteor/app/apps/server/bridges/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IAppServerOrchestrator } from '@rocket.chat/apps';
import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages';
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
import type { IRoom, IRoomRaw } from '@rocket.chat/apps-engine/definition/rooms';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
import type { GetMessagesOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings';
import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models';
Expand All @@ -17,6 +17,41 @@ import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFrom
import { createChannelMethod } from '../../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup';

const rawRoomProjection: FindOptions<ICoreRoom>['projection'] = {
_id: 1,
fname: 1,
name: 1,
usernames: 1,
members: 1,
uids: 1,
default: 1,
ro: 1,
sysMes: 1,
msgs: 1,
ts: 1,
_updatedAt: 1,
closedAt: 1,
lm: 1,
description: 1,
customFields: 1,
prid: 1,
teamId: 1,
teamMain: 1,
livechatData: 1,
waitingResponse: 1,
open: 1,
source: 1,
closer: 1,
t: 1,
u: 1,
v: 1,
contactId: 1,
departmentId: 1,
closedBy: 1,
servedBy: 1,
responseBy: 1,
};

export class AppRoomBridge extends RoomBridge {
constructor(private readonly orch: IAppServerOrchestrator) {
super();
Expand Down Expand Up @@ -151,6 +186,37 @@ export class AppRoomBridge extends RoomBridge {
return promises as Promise<IUser[]>;
}

protected async getAllRooms(filters: GetRoomsFilters = {}, options: GetRoomsOptions = {}, appId: string): Promise<Array<IRoomRaw>> {
this.orch.debugLog(`The App ${appId} is getting all rooms with options`, options);

const { limit = 100, skip = 0 } = options;

const findOptions: FindOptions<ICoreRoom> = {
sort: { ts: -1 },
skip,
limit: Math.min(limit, 100),
projection: rawRoomProjection,
};

const { types, discussions, teams } = filters;

const rooms: IRoomRaw[] = [];

const roomConverter = this.orch.getConverters()?.get('rooms');
if (!roomConverter) {
throw new Error('Room converter not found');
}

for await (const room of Rooms.findAllByTypesAndDiscussionAndTeam({ types, discussions, teams }, findOptions)) {
const converted = await roomConverter.convertRoomRaw(room);
if (converted) {
rooms.push(converted);
}
}

return rooms;
}

protected async getDirectByUsernames(usernames: Array<string>, appId: string): Promise<IRoom | undefined> {
this.orch.debugLog(`The App ${appId} is getting direct room by usernames: "${usernames}"`);
const room = await Rooms.findDirectRoomContainingAllUsernames(usernames, {});
Expand Down
103 changes: 103 additions & 0 deletions apps/meteor/app/apps/server/converters/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,106 @@ export class AppRoomsConverter {
return this.convertRoom(room);
}

convertRoomRaw(room) {
if (!room) {
return undefined;
}

const mapUserLookup = (user) =>
user && {
_id: user._id ?? user.id,
...(user.username && { username: user.username }),
...(user.name && { name: user.name }),
};

const map = {
id: '_id',
displayName: 'fname',
slugifiedName: 'name',
members: 'members',
userIds: 'uids',
usernames: 'usernames',
messageCount: 'msgs',
createdAt: 'ts',
updatedAt: '_updatedAt',
closedAt: 'closedAt',
lastModifiedAt: 'lm',
customFields: 'customFields',
livechatData: 'livechatData',
isWaitingResponse: 'waitingResponse',
isOpen: 'open',
description: 'description',
source: 'source',
closer: 'closer',
teamId: 'teamId',
isTeamMain: 'teamMain',
isDefault: 'default',
isReadOnly: 'ro',
contactId: 'contactId',
departmentId: 'departmentId',
parentRoomId: 'prid',
visitor: (data) => {
const { v } = data;
if (!v) {
return undefined;
}

delete data.v;

const { _id: id, ...rest } = v;

return {
id,
...rest,
};
},
displaySystemMessages: (data) => {
const { sysMes } = data;
delete data.sysMes;
return typeof sysMes === 'undefined' ? true : sysMes;
},
type: (data) => {
const result = this._convertTypeToApp(data.t);
delete data.t;
return result;
},
creator: (data) => {
if (!data.u) {
return undefined;
}
const creator = mapUserLookup(data.u);
delete data.u;
return creator;
},
closedBy: (data) => {
if (!data.closedBy) {
return undefined;
}
const { closedBy } = data;
delete data.closedBy;
return mapUserLookup(closedBy);
},
servedBy: (data) => {
if (!data.servedBy) {
return undefined;
}
const { servedBy } = data;
delete data.servedBy;
return mapUserLookup(servedBy);
},
responseBy: (data) => {
if (!data.responseBy) {
return undefined;
}
const { responseBy } = data;
delete data.responseBy;
return mapUserLookup(responseBy);
},
};

return transformMappedData(room, map);
}

async __getCreator(user) {
if (!user) {
return;
Expand Down Expand Up @@ -54,6 +154,7 @@ export class AppRoomsConverter {
username: visitor.username,
token: visitor.token,
status: visitor.status || 'online',
activity: visitor.activity,
...(lastMessageTs && { lastMessageTs }),
...(phone && { phone }),
};
Expand Down Expand Up @@ -200,6 +301,8 @@ export class AppRoomsConverter {
description: 'description',
source: 'source',
closer: 'closer',
teamId: 'teamId',
isTeamMain: 'teamMain',
isDefault: (room) => {
const result = !!room.default;
delete room.default;
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/apps/server/converters/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class AppVisitorsConverter {
visitorEmails: 'visitorEmails',
livechatData: 'livechatData',
status: 'status',
activity: 'activity',
};

return transformMappedData(visitor, map);
Expand Down
11 changes: 9 additions & 2 deletions packages/apps-engine/src/definition/accessors/IRoomRead.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { GetMessagesOptions } from '../../server/bridges/RoomBridge';
import type { GetMessagesOptions, GetRoomsFilters, GetRoomsOptions } from '../../server/bridges/RoomBridge';
import type { IMessageRaw } from '../messages/index';
import type { IRoom } from '../rooms/index';
import type { IRoom, IRoomRaw } from '../rooms/index';
import type { IUser } from '../users/index';

/**
Expand Down Expand Up @@ -61,6 +61,13 @@ export interface IRoomRead {
*/
getMembers(roomId: string): Promise<Array<IUser>>;

/**
* Retrieves rooms in the workspace, optionally filtered by type and whether they are discussions or part of teams.
*
* @returns a list of raw rooms, or undefined if the app does not have the permission to view all rooms
*/
getAllRooms(filters?: GetRoomsFilters, options?: GetRoomsOptions): Promise<Array<IRoomRaw> | undefined>;

/**
* Gets a direct room with all usernames
* @param usernames all usernames belonging to the direct room
Expand Down
1 change: 1 addition & 0 deletions packages/apps-engine/src/definition/livechat/IVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface IVisitor {
phone?: Array<IVisitorPhone>;
visitorEmails?: Array<IVisitorEmail>;
status?: string;
activity?: string[];
customFields?: { [key: string]: any };
livechatData?: { [key: string]: any };
}
2 changes: 2 additions & 0 deletions packages/apps-engine/src/definition/rooms/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface IRoom {
slugifiedName: string;
type: RoomType;
creator: IUser;
teamId?: string;
isTeamMain?: boolean;
/**
* @deprecated usernames will be removed on version 2.0.0
*/
Expand Down
43 changes: 43 additions & 0 deletions packages/apps-engine/src/definition/rooms/IRoomRaw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { IVisitor } from '../livechat';
import type { IOmnichannelSource, IVisitorChannelInfo } from '../livechat/ILivechatRoom';
import type { IUserLookup } from '../users';
import type { RoomType } from './RoomType';

/**
* A lightweight representation of a room without resolving relational data.
* This is intended for listing operations to avoid additional database lookups.
*/
export interface IRoomRaw {
id: string;
slugifiedName: string;
displayName?: string;
type: RoomType;
creator?: IUserLookup;
members?: Array<string>;
userIds?: Array<string>;
usernames?: Array<string>;
isDefault?: boolean;
isReadOnly?: boolean;
displaySystemMessages?: boolean;
messageCount?: number;
createdAt?: Date;
updatedAt?: Date;
closedAt?: Date;
lastModifiedAt?: Date;
description?: string;
customFields?: { [key: string]: any };
parentRoomId?: string;
teamId?: string;
isTeamMain?: boolean;
livechatData?: { [key: string]: any };
isWaitingResponse?: boolean;
isOpen?: boolean;
closer?: 'user' | 'visitor' | 'bot';
closedBy?: IUserLookup;
servedBy?: IUserLookup;
responseBy?: IUserLookup;
source?: IOmnichannelSource;
visitor?: Pick<IVisitor, 'id' | 'token' | 'username' | 'name' | 'status' | 'activity'> & IVisitorChannelInfo;
departmentId?: string;
contactId?: string;
}
2 changes: 2 additions & 0 deletions packages/apps-engine/src/definition/rooms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { IPreRoomCreateModify } from './IPreRoomCreateModify';
import { IPreRoomCreatePrevent } from './IPreRoomCreatePrevent';
import { IPreRoomDeletePrevent } from './IPreRoomDeletePrevent';
import { IRoom } from './IRoom';
import { IRoomRaw } from './IRoomRaw';
import { RoomType } from './RoomType';

export {
IRoom,
IRoomRaw,
RoomType,
IPostRoomCreate,
IPostRoomDeleted,
Expand Down
20 changes: 18 additions & 2 deletions packages/apps-engine/src/server/accessors/RoomRead.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IRoomRead } from '../../definition/accessors';
import type { IMessageRaw } from '../../definition/messages';
import type { IRoom } from '../../definition/rooms';
import type { IRoom, IRoomRaw } from '../../definition/rooms';
import type { IUser } from '../../definition/users';
import type { RoomBridge } from '../bridges';
import { type GetMessagesOptions, GetMessagesSortableFields } from '../bridges/RoomBridge';
import { type GetMessagesOptions, type GetRoomsFilters, type GetRoomsOptions, GetMessagesSortableFields } from '../bridges/RoomBridge';

export class RoomRead implements IRoomRead {
constructor(
Expand Down Expand Up @@ -46,6 +46,22 @@ export class RoomRead implements IRoomRead {
return this.roomBridge.doGetMembers(roomId, this.appId);
}

public getAllRooms(filters: GetRoomsFilters = {}, { limit = 100, skip = 0 }: GetRoomsOptions = {}): Promise<Array<IRoomRaw> | undefined> {
if (!Number.isFinite(limit) || limit <= 0 || limit > 100) {
throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`);
}

if (!Number.isFinite(skip) || skip < 0) {
throw new Error(`Invalid skip provided. Expected number >= 0, got ${skip}`);
}

return this.roomBridge.doGetAllRooms(
filters,
{ limit, skip },
this.appId,
);
}

public getDirectByUsernames(usernames: Array<string>): Promise<IRoom> {
return this.roomBridge.doGetDirectByUsernames(usernames, this.appId);
}
Expand Down
Loading
Loading