Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 room listing to the Apps Engine bridge to let apps retrieve rooms by type and provide workspace-wide visibility.
Comment thread
Dnouv marked this conversation as resolved.
Outdated
111 changes: 108 additions & 3 deletions apps/meteor/app/apps/server/bridges/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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, 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';
import type { FindOptions, Sort } from 'mongodb';
import type { FindOptions, Filter, Sort } from 'mongodb';

import { createDirectMessage } from '../../../../server/methods/createDirectMessage';
import { createDiscussion } from '../../../discussion/server/methods/createDiscussion';
Expand Down Expand Up @@ -151,6 +151,36 @@ export class AppRoomBridge extends RoomBridge {
return promises as Promise<IUser[]>;
}

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

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

const { query } = this.buildRoomQuery(types);

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

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.find(query, findOptions)) {
const converted = await roomConverter.convertRoomRaw(room);
if (converted) {
rooms.push(converted);
}
}

return rooms;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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 Expand Up @@ -314,4 +344,79 @@ export class AppRoomBridge extends RoomBridge {
const members = await Users.findUsersByUsernames(usernames, { limit: 50 }).toArray();
await Promise.all(members.map((user) => removeUserFromRoom(roomId, user)));
}

private buildRoomQuery(types?: Array<RoomType>): { query: Filter<ICoreRoom> } {
if (!types || types.length === 0) {
return { query: {} };
}

const baseTypes = new Set<ICoreRoom['t']>();
let includeDiscussions = false;
let includeTeams = false;

for (const type of types) {
if (type === RoomType.DISCUSSION) {
includeDiscussions = true;
continue;
}

if (type === RoomType.TEAM) {
includeTeams = true;
continue;
}

if (type === RoomType.CHANNEL) {
baseTypes.add('c');
continue;
}

if (type === RoomType.PRIVATE_GROUP) {
baseTypes.add('p');
continue;
}

if (type === RoomType.DIRECT_MESSAGE) {
baseTypes.add('d');
continue;
}

if (type === RoomType.LIVE_CHAT) {
baseTypes.add('l');
Comment thread
d-gubert marked this conversation as resolved.
Outdated
}
}

const conditions: Array<Filter<ICoreRoom>> = [];

if (baseTypes.size) {
const baseCondition: Filter<ICoreRoom> = { t: { $in: Array.from(baseTypes) } };

if (!includeDiscussions) {
baseCondition.prid = { $exists: false };
}

if (!includeTeams) {
baseCondition.teamMain = { $ne: true };
}

conditions.push(baseCondition);
Comment thread
d-gubert marked this conversation as resolved.
Outdated
}

if (includeDiscussions) {
conditions.push({ prid: { $exists: true } });
}

if (includeTeams) {
conditions.push({ teamMain: true });
}

if (!conditions.length) {
return { query: {} };
}

if (conditions.length === 1) {
return { query: conditions[0] };
}

return { query: { $or: conditions } };
}
}
96 changes: 93 additions & 3 deletions apps/meteor/app/apps/server/converters/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,44 @@ export class AppRoomsConverter {
return this.convertRoom(room);
}

async convertRoomRaw(room) {
Comment thread
Dnouv marked this conversation as resolved.
Outdated
if (!room) {
return undefined;
}

const creator = room.u
? {
_id: room.u._id,
username: room.u.username,
name: room.u.name,
}
: undefined;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const type = this._resolveRoomType(room);

return {
id: room._id,
slugifiedName: room.name || room.fname || room._id,
Comment thread
Dnouv marked this conversation as resolved.
Outdated
displayName: room.fname,
type,
creator,
userIds: room.uids,
isDefault: !!room.default,
isReadOnly: !!room.ro,
displaySystemMessages: typeof room.sysMes === 'undefined' ? true : room.sysMes,
messageCount: room.msgs,
createdAt: room.ts,
updatedAt: room._updatedAt,
lastModifiedAt: room.lm,
description: room.description,
customFields: room.customFields,
parentRoomId: room.prid,
teamId: room.teamId,
isTeamMain: !!room.teamMain,
livechatData: room.livechatData,
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async __getCreator(user) {
if (!user) {
return;
Expand Down Expand Up @@ -176,11 +214,15 @@ export class AppRoomsConverter {
return newRoom;
}

async convertRoom(originalRoom) {
async convertRoom(originalRoom, { lightweight = false } = {}) {
if (!originalRoom) {
return undefined;
}

if (lightweight) {
return this.convertRoomWithoutLookups(originalRoom);
}
Comment thread
Dnouv marked this conversation as resolved.
Outdated

const map = {
id: '_id',
displayName: 'fname',
Expand Down Expand Up @@ -221,9 +263,9 @@ export class AppRoomsConverter {
return sysMes;
},
type: (room) => {
const result = this._convertTypeToApp(room.t);
const derivedType = this._resolveRoomType(room);
delete room.t;
return result;
return derivedType;
},
creator: async (room) => {
const { u } = room;
Expand Down Expand Up @@ -337,6 +379,54 @@ export class AppRoomsConverter {
return transformMappedData(originalRoom, map);
}

convertRoomWithoutLookups(room) {
Comment thread
Dnouv marked this conversation as resolved.
Outdated
const creatorFromRoom = room.u
? {
_id: room.u._id,
username: room.u.username,
name: room.u.name || room.u.username || 'Unknown',
}
: undefined;

const fallbackUser = {
_id: 'unknown',
username: 'unknown',
name: 'Unknown',
};
Comment thread
Dnouv marked this conversation as resolved.
Outdated

return {
id: room._id,
displayName: room.fname,
slugifiedName: room.name || room.fname || room._id,
type: this._resolveRoomType(room),
creator: creatorFromRoom || fallbackUser,
Comment thread
Dnouv marked this conversation as resolved.
Outdated
usernames: room.usernames || [],
Comment thread
Dnouv marked this conversation as resolved.
Outdated
userIds: room.uids || [],
isDefault: !!room.default,
isReadOnly: !!room.ro,
displaySystemMessages: typeof room.sysMes === 'undefined' ? true : room.sysMes,
messageCount: room.msgs,
createdAt: room.ts,
updatedAt: room._updatedAt,
lastModifiedAt: room.lm,
description: room.description,
customFields: room.customFields,
livechatData: room.livechatData,
Comment thread
Dnouv marked this conversation as resolved.
Outdated
};
}

_resolveRoomType(room) {
if (room.teamMain) {
return RoomType.TEAM;
}

if (room.prid) {
return RoomType.DISCUSSION;
}
Comment thread
Dnouv marked this conversation as resolved.
Outdated

return this._convertTypeToApp(room.t);
}

_convertTypeToApp(typeChar) {
switch (typeChar) {
case 'c':
Expand Down
12 changes: 10 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, 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,14 @@ export interface IRoomRead {
*/
getMembers(roomId: string): Promise<Array<IUser>>;

/**
* Retrieves all rooms in the workspace, optionally filtered by type or other flags.
*
* @param options Optional filters such as types and pagination
* @returns a list of lightweight rooms
*/
getAllRooms(options?: Partial<GetRoomsOptions>): Promise<Array<IRoomRaw>>;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* Gets a direct room with all usernames
* @param usernames all usernames belonging to the direct room
Expand Down
28 changes: 28 additions & 0 deletions packages/apps-engine/src/definition/rooms/IRoomRaw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.
Comment thread
d-gubert marked this conversation as resolved.
*/
export interface IRoomRaw {
id: string;
slugifiedName: string;
displayName?: string;
type: RoomType;
creator?: IUserLookup;
userIds?: Array<string>;
isDefault?: boolean;
isReadOnly?: boolean;
displaySystemMessages?: boolean;
messageCount?: number;
createdAt?: Date;
updatedAt?: Date;
lastModifiedAt?: Date;
description?: string;
customFields?: { [key: string]: any };
parentRoomId?: string;
teamId?: string;
isTeamMain?: boolean;
Comment thread
Dnouv marked this conversation as resolved.
livechatData?: { [key: string]: any };
}
2 changes: 2 additions & 0 deletions packages/apps-engine/src/definition/rooms/RoomType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export enum RoomType {
PRIVATE_GROUP = 'p',
DIRECT_MESSAGE = 'd',
LIVE_CHAT = 'l',
DISCUSSION = 'discussion',
TEAM = 'team',
Comment thread
Dnouv marked this conversation as resolved.
Outdated
}
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
25 changes: 23 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 GetRoomsOptions, GetMessagesSortableFields } from '../bridges/RoomBridge';

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

public getAllRooms(options: Partial<GetRoomsOptions> = {}): Promise<Array<IRoomRaw>> {
Comment thread
Dnouv marked this conversation as resolved.
Outdated
const limit = options.limit ?? 100;

if (!Number.isFinite(limit) || limit <= 0 || limit > 100) {
throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`);
Comment thread
d-gubert marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

return this.roomBridge.doGetAllRooms(
{
...options,
limit,
skip: options.skip ?? 0,
},
this.appId,
);
}

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