Skip to content

feat: make roomId optional in getMentionedMessages and getStarredMessages to support Activity Hub global fetch #39651

@CodeUltr0n

Description

@CodeUltr0n

Description:

The chat.getMentionedMessages and chat.getStarredMessages REST API endpoints
currently require a roomId parameter. This makes it impossible for the Activity
Hub to fetch mentions and starred messages globally across all of a user's subscribed
rooms in a single request.

Affected file 1 — apps/meteor/app/api/server/lib/messages.ts

Both findMentionedMessages and findStarredMessages have roomId typed as
string (required), with no global fallback path. If roomId is not provided,
the function immediately fails at the room access check:

// findMentionedMessages
export async function findMentionedMessages({
	uid,
	roomId,
	pagination: { offset, count, sort },
}: {
	uid: string;
	roomId: string; // required — no optional global path exists
	pagination: { offset: number; count: number; sort: FindOptions<IMessage>['sort'] };
}) {
	const room = await Rooms.findOneById(roomId);
	if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) {
		throw new Error('error-not-allowed');
	}
	// only ever calls the room-scoped model method
	const { cursor, totalCount } = Messages.findPaginatedVisibleByMentionAndRoomId(user.username, roomId, {
		sort: sort || { ts: -1 },
		skip: offset,
		limit: count,
	});
}
// findStarredMessages
export async function findStarredMessages({
	uid,
	roomId,
	pagination: { offset, count, sort },
}: {
	uid: string;
	roomId: string; // required — no optional global path exists
	pagination: { offset: number; count: number; sort: FindOptions<IMessage>['sort'] };
}) {
	const room = await Rooms.findOneById(roomId);
	if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) {
		throw new Error('error-not-allowed');
	}
	// only ever calls the room-scoped model method
	const { cursor, totalCount } = Messages.findStarredByUserAtRoom(uid, roomId, {
		sort: sort || { ts: -1 },
		skip: offset,
		limit: count,
	});
}

Affected file 2 — packages/models/src/models/Messages.ts

The model only has room-scoped methods. There are no global counterparts that
can query across multiple rooms for a given user:

// only room-scoped method exists for mentions
findPaginatedVisibleByMentionAndRoomId(
	username: IUser['username'],
	rid: IRoom['_id'],
	options?: FindOptions<IMessage>,
): FindPaginated<FindCursor<IMessage>> {
	const query: Filter<IMessage> = {
		'_hidden': { $ne: true },
		'mentions.username': username,
		rid, // always scoped to a single room
	};
	return this.findPaginated(query, options);
}

// only room-scoped method exists for starred
findStarredByUserAtRoom(
	userId: IUser['_id'],
	roomId: IRoom['_id'],
	options?: FindOptions<IMessage>,
): FindPaginated<FindCursor<IMessage>> {
	const query: Filter<IMessage> = {
		'_hidden': { $ne: true },
		'starred._id': userId,
		'rid': roomId, // always scoped to a single room
	};
	return this.findPaginated(query, options);
}

Affected file 3 — packages/models/src/models/BaseRaw.ts

BaseRaw has no aggregatePaginated method. Only findPaginated exists, which
uses a simple find() + countDocuments() and cannot support cross-room
aggregation pipelines needed for a global query:

findPaginated(query: Filter<T> = {}, options?: any): FindPaginated<FindCursor<WithId<T>>> {
	const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
	const cursor = optionsDef ? this.col.find(query, optionsDef) : this.col.find(query);
	const totalCount = this.col.countDocuments(query);
	return {
		cursor,
		totalCount,
	};
}
// no aggregatePaginated method exists

A note on user privacy:

While implementing the global fetch, we want to make sure the Activity Hub only
shows messages from rooms that are part of a user's normal chat experience.

Rocket.Chat has different room types, each stored as a t field on the
subscription document:

t Room type Should appear in Activity Hub
c Public channel Yes
p Private group Yes
d Direct message Yes
l Livechat No
v VoIP No

Livechat rooms (l) are customer support conversations that belong to agents
and managers — a regular user should never see those messages appearing in their
personal Activity Hub feed. VoIP rooms (v) are call records and not chat
messages, so they have no place in a mentions or starred messages view either.

The global subscription fetch should filter to only include the standard room
types (c, p, d) so that every user's Activity Hub stays within the
boundaries of what they are actually meant to see. This keeps the privacy model
consistent with the existing per-room checks already in place for the
room-scoped path.


Steps to reproduce:

  1. Log in as any user who is subscribed to multiple rooms
  2. Call GET api/v1/chat.getMentionedMessages without a roomId parameter
  3. Call GET api/v1/chat.getStarredMessages without a roomId parameter

Expected behavior:

Both endpoints should support an optional roomId. When roomId is omitted,
the API should return all mentioned/starred messages across every room the user
is subscribed to, paginated, sorted by timestamp descending. This is required
for the Activity Hub to display a unified cross-room feed in a single request.

Actual behavior:

Both endpoints require roomId. There is no global path in the service layer,
no global query methods in the model layer, and no base aggregation support in
BaseRaw. As a result, a client wanting to power the Activity Hub must fire one
separate API request per subscribed room and merge results client-side — which
for a user subscribed to 50 rooms means 50 HTTP round trips per Activity Hub load
instead of one.

Server Setup Information:

  • Version of Rocket.Chat Server: latest
  • License Type: Community / Enterprise
  • Number of Users: any deployment where users are subscribed to many rooms
  • Operating System: any
  • Deployment Method: any
  • Number of Running Instances: any
  • DB Replicaset Oplog: any
  • NodeJS Version: any
  • MongoDB Version: any

Client Setup Information

  • Desktop App or Browser Version: any
  • Operating System: any

Additional context

This is a prerequisite for the Activity Hub GSoC feature. The Activity Hub is
intended to be a single view showing mentions, starred messages, and discussions
across all of a user's rooms. Without a global API path, every page load of the
Activity Hub requires N sequential or parallel HTTP calls (one per subscribed room),
which creates unnecessary backend stress and poor UX latency that scales linearly
with the number of rooms a user has joined.

The three files that need to change to support this are:

  • apps/meteor/app/api/server/lib/messages.ts — make roomId optional, add
    global branch using Subscriptions.findByUserId
  • packages/models/src/models/Messages.ts — add global query methods
    findPaginatedVisibleByMention and findStarredByUser
  • packages/models/src/models/BaseRaw.ts — add aggregatePaginated to support
    paginated aggregation pipelines at the base model level

Relevant logs:

No runtime error is thrown. The current behaviour silently fails for clients
that omit roomId — the request is rejected at the room access check with
error-not-allowed because roomId is undefined, giving no indication that
a global path is simply not implemented.

The Rocket.Chat monorepo requires a full Docker + MongoDB replica set to run
locally. The failure is verifiable directly from the source — roomId is typed
as string (required) in messages.ts, so any request without it is rejected
at the canAccessRoomAsync check before any database query runs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gsoc-projecttriagedIssue reviewed and properly taggedtype: featurePull requests that introduces new feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions