Skip to content

Conversation

@rodrigok
Copy link
Member

@rodrigok rodrigok commented Oct 15, 2025

https://rocketchat.atlassian.net/browse/FDR-232

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability and error handling for joining rooms via invites, reducing failures and inconsistencies.
  • Refactor

    • Split and streamlined join flows to better handle local vs. remote joins, improving local join speed and clarity.
  • Chores

    • Removed an internal HTTP join route and consolidated join handling through the updated invite-based flow.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 15, 2025

Walkthrough

joinUser in packages/federation-sdk/src/services/room.service.ts now accepts an invite: PersistentEventBase and branches to a local join flow when the invite sender is local, otherwise uses the federated join flow. The internal PUT join route in packages/homeserver/src/controllers/internal/room.controller.ts was removed/disabled.

Changes

Cohort / File(s) Summary
Room service: join flow & signature
packages/federation-sdk/src/services/room.service.ts
joinUser signature changed to (invite: PersistentEventBase, userId: UserID). Extracts roomId and residentServer from the invite and branches: local resident -> local join path (load latest state, find create event, build/store membership event, emit membership, propagate); remote resident -> federated join via federationService.makeJoin, reconcile/process returned state, persist final join event. Error handling updated accordingly.
Internal controller: join route removed
packages/homeserver/src/controllers/internal/room.controller.ts
The internal PUT route handler for /internal/rooms/:roomId/join/:userId was removed/disabled (handler invocation and response logic commented out or deleted), disabling the internal join endpoint.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Internal Caller
  participant RoomSvc as RoomService
  participant StateSvc as StateService
  participant EventBuilder as Event Builder
  participant Emitter as EventEmitter
  participant FedSvc as FederationService
  participant Store as Persistence

  Caller->>RoomSvc: joinUser(invite {roomId, sender}, userId)

  alt invite.sender is local
    RoomSvc->>StateSvc: getLatestState(roomId)
    StateSvc-->>RoomSvc: latest state (includes create)
    RoomSvc->>EventBuilder: build local membership join event
    EventBuilder-->>RoomSvc: joinEvent
    RoomSvc->>Store: persist joinEvent
    RoomSvc->>Emitter: emit membership event
    RoomSvc->>FedSvc: propagate join to other servers
    FedSvc-->>RoomSvc: propagate result
    RoomSvc-->>Caller: success/failure
  else invite.sender is remote
    RoomSvc->>FedSvc: makeJoin(roomId, userId)
    FedSvc-->>RoomSvc: join response + state
    RoomSvc->>StateSvc: reconcile/process returned state
    StateSvc-->>RoomSvc: final joinEvent
    RoomSvc->>Store: persist final joinEvent
    RoomSvc-->>Caller: success/failure
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

Poem

A rabbit nibbles at an invite note,
If burrow's ours, I stitch the welcome coat.
If distant warren sent the call,
I hop the lanes and federate to all.
Hops and events — a tidy, joyful bolt 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "fix: can't add local users on remote rooms" directly addresses the core issue described in FDR-232: enabling an external homeserver owner to add users from their own server to a federated room. The changes show a refactored joinUser method that distinguishes between local and remote server joins based on resident server matching, which is precisely the logic needed to resolve the 500 error experienced when a FED2 owner attempted to add another FED2 user. The title is concise, specific, and accurately captures the primary fix being implemented.
Linked Issues Check ✅ Passed The code changes directly address the requirements in FDR-232. The modified joinUser signature now accepts an invite object containing the resident server information, and the implementation splits the join logic based on whether the resident server matches the local server. When a FED2 owner adds a FED2 user, the residentServer derived from invite.sender will match the local server, enabling a local-join flow that emits membership events and propagates the join to other servers, rather than failing with the previous 500 error. This matches the expected behavior: the external owner can add same-homeserver users successfully and the membership event propagates to all homeservers.
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly related to fixing the FDR-232 issue. The refactored joinUser method signature and its implementation that distinguish between local and remote server joins are essential to resolving the problem. The removal of the PUT route for joining a room in the controller appears to be a necessary cleanup related to the API change from accepting roomId/userId parameters to accepting an invite event. No modifications unrelated to the stated objective of enabling same-homeserver user additions to federated rooms were detected.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/add-users-from-same-server-on-remote-rooms

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

codecov-commenter commented Oct 15, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.55%. Comparing base (5a841a8) to head (cb4a180).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #281   +/-   ##
=======================================
  Coverage   60.55%   60.55%           
=======================================
  Files          67       67           
  Lines        6691     6691           
=======================================
  Hits         4052     4052           
  Misses       2639     2639           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rodrigok rodrigok force-pushed the fix/add-users-from-same-server-on-remote-rooms branch 2 times, most recently from a43cd9c to 61c35f4 Compare October 15, 2025 23:48
sampaiodiego
sampaiodiego previously approved these changes Oct 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/federation-sdk/src/services/room.service.ts (1)

809-822: Use the room's actual version instead of the default.

Line 821 uses PersistentEventFactory.defaultRoomVersion, but the room may use a different version. Other methods in this file (e.g., lines 608, 689, 771) correctly retrieve the room version via stateService.getRoomVersion(roomId). Using the wrong version can cause auth validation failures, event format mismatches, and federation issues.

Apply this diff to use the correct room version:

 		const room = await stateService.getLatestRoomState(roomId);

 		const createEvent = room.get('m.room.create:');

 		if (!createEvent) {
 			throw new Error(
 				'Room create event not found when trying to join a room',
 			);
 		}

+		const roomVersion = await stateService.getRoomVersion(roomId);
+
 		const membershipEvent = await stateService.buildEvent<'m.room.member'>(
 			{
 				type: 'm.room.member',
 				content: { membership: 'join' },
 				room_id: roomId,
 				state_key: userId,
 				auth_events: [],
 				depth: 0,
 				prev_events: [],
 				origin_server_ts: Date.now(),
 				sender: userId,
 			},
-			PersistentEventFactory.defaultRoomVersion,
+			roomVersion,
 		);
🧹 Nitpick comments (3)
packages/federation-sdk/src/services/room.service.ts (3)

794-794: Clarify the variable name for better code readability.

The variable residentServer is misleading—it extracts the domain from the invite sender, not the room's resident/hosting server. In a federated room hosted on FED1, if a FED2 user sends the invite, this variable will be FED2. Consider renaming to inviteSenderServer or senderDomain to avoid confusion.

Apply this diff:

-		const residentServer = extractDomainFromId(invite.sender);
+		const senderDomain = extractDomainFromId(invite.sender);

		// our own room, we can validate the join event by ourselves
		// once done, emit the event to all participating servers
-		if (residentServer === configService.serverName) {
+		if (senderDomain === configService.serverName) {

(And update the corresponding reference at line 849.)


826-834: Extract event properties from the built event for consistency.

The event emission hardcodes several properties (content, sender, origin_server_ts) instead of extracting them from membershipEvent.event. If buildEvent modifies these values, the emitted event will contain inconsistent data.

Apply this diff to extract all properties from the built event:

 		this.eventEmitterService.emit('homeserver.matrix.membership', {
 			event_id: membershipEvent.eventId,
 			event: membershipEvent.event,
 			room_id: roomId,
 			state_key: userId,
-			content: { membership: 'join' },
-			sender: userId,
-			origin_server_ts: Date.now(),
+			content: membershipEvent.event.content,
+			sender: membershipEvent.event.sender,
+			origin_server_ts: membershipEvent.event.origin_server_ts,
 		});

787-794: Consider validating the invite event.

The method accepts a PersistentEventBase as invite but doesn't validate that:

  1. It's actually an invite event (type m.room.member with content.membership === 'invite')
  2. The state_key matches the userId being joined
  3. The invite is still valid (not expired or revoked)

While these checks may exist upstream, adding defensive validation here would make the function more robust and catch misuse earlier.

Example validation to add after line 794:

// Validate invite event
if (invite.event.type !== 'm.room.member' || 
    invite.event.content?.membership !== 'invite') {
	throw new Error('Invalid invite: expected m.room.member event with membership=invite');
}

if (invite.event.state_key !== userId) {
	throw new Error(`Invalid invite: state_key ${invite.event.state_key} does not match userId ${userId}`);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b6fef3e and 61c35f4.

📒 Files selected for processing (2)
  • packages/federation-sdk/src/services/room.service.ts (1 hunks)
  • packages/homeserver/src/controllers/internal/room.controller.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/homeserver/src/controllers/internal/room.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/room.service.ts (2)
packages/room/src/types/_common.ts (1)
  • UserID (20-20)
packages/room/src/manager/event-wrapper.ts (2)
  • roomId (101-103)
  • extractDomainFromId (23-29)

@rodrigok rodrigok force-pushed the fix/add-users-from-same-server-on-remote-rooms branch from 61c35f4 to cb4a180 Compare October 15, 2025 23:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/federation-sdk/src/services/room.service.ts (1)

845-845: Use PersistentEventFactory.defaultRoomVersion instead of hardcoded room version.

Line 845 hardcodes roomVersion = '10' while the rest of the codebase, including the local join path at line 821, uses PersistentEventFactory.defaultRoomVersion. While both currently equal '10', using the constant ensures consistency and prevents maintenance issues if the default version changes.

🧹 Nitpick comments (3)
packages/federation-sdk/src/services/room.service.ts (3)

799-799: Add error handling for room state retrieval.

The call to getLatestRoomState(roomId) could throw an error if the room doesn't exist locally. Consider adding error handling to provide a clearer error message.

Apply this diff:

-	const room = await stateService.getLatestRoomState(roomId);
+	let room;
+	try {
+		room = await stateService.getLatestRoomState(roomId);
+	} catch (error) {
+		this.logger.error(
+			{ roomId, error },
+			'Failed to get room state for local join',
+		);
+		throw new HttpException(
+			'Room not found or inaccessible',
+			HttpStatus.NOT_FOUND,
+		);
+	}

798-843: Add logging to the local join path.

The local join path lacks detailed logging compared to the remote join path (lines 880-955). Adding logs would help with debugging and monitoring.

Consider adding logging at key points:

 if (residentServer === configService.serverName) {
+	this.logger.info(
+		{ roomId, userId, inviteSender: invite.sender },
+		'Processing local join for user on same server as invite sender',
+	);
+
 	const room = await stateService.getLatestRoomState(roomId);
 
 	const createEvent = room.get('m.room.create:');
 
 	if (!createEvent) {
+		this.logger.error({ roomId }, 'Create event not found for local join');
 		throw new Error(
 			'Room create event not found when trying to join a room',
 		);
 	}
 
 	const membershipEvent = await stateService.buildEvent<'m.room.member'>(

809-822: Consider extracting membership event building into a helper method.

The logic for building membership events is duplicated in multiple places (lines 243-258, 596-609, 674-690, 756-772, 809-822). Extracting this into a helper method would improve maintainability and reduce code duplication.

Example helper method:

private async buildMembershipEvent(
	roomId: RoomID,
	userId: UserID,
	membership: 'join' | 'leave' | 'invite' | 'ban',
	roomVersion: RoomVersion,
	options?: {
		reason?: string;
		displayname?: string;
		is_direct?: boolean;
	}
): Promise<PersistentEventBase> {
	return this.stateService.buildEvent<'m.room.member'>(
		{
			type: 'm.room.member',
			content: {
				membership,
				...options,
			},
			room_id: roomId,
			state_key: userId,
			auth_events: [],
			depth: 0,
			prev_events: [],
			origin_server_ts: Date.now(),
			sender: userId,
		},
		roomVersion,
	);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 61c35f4 and cb4a180.

📒 Files selected for processing (2)
  • packages/federation-sdk/src/services/room.service.ts (1 hunks)
  • packages/homeserver/src/controllers/internal/room.controller.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/homeserver/src/controllers/internal/room.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/room.service.ts (2)
packages/federation-sdk/src/index.ts (2)
  • PersistentEventBase (25-25)
  • UserID (28-28)
packages/room/src/manager/event-wrapper.ts (2)
  • roomId (101-103)
  • extractDomainFromId (23-29)

Comment on lines +787 to +794
async joinUser(invite: PersistentEventBase, userId: UserID) {
const configService = this.configService;
const stateService = this.stateService;
const federationService = this.federationService;
const roomId = invite.roomId;

// where the room is hosted at
const residentServer = extractDomainFromId(roomId);
const residentServer = extractDomainFromId(invite.sender);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate invite parameter and userId matching.

The method accepts an invite parameter but doesn't validate that it's a proper invite event. Consider adding validation to ensure:

  1. invite.type === 'm.room.member'
  2. invite.content.membership === 'invite'
  3. userId === invite.stateKey (to ensure we're joining the correct user)

Without these checks, the method could process invalid invites or join the wrong user.

Apply this diff to add validation at the beginning of the method:

 async joinUser(invite: PersistentEventBase, userId: UserID) {
+	// Validate that the invite is a proper invite event
+	if (invite.type !== 'm.room.member') {
+		throw new HttpException(
+			'Invalid invite: expected m.room.member event',
+			HttpStatus.BAD_REQUEST,
+		);
+	}
+	
+	if (invite.content.membership !== 'invite') {
+		throw new HttpException(
+			'Invalid invite: expected membership to be invite',
+			HttpStatus.BAD_REQUEST,
+		);
+	}
+	
+	if (invite.stateKey !== userId) {
+		throw new HttpException(
+			'Invalid invite: userId does not match invite state_key',
+			HttpStatus.BAD_REQUEST,
+		);
+	}
+
 	const configService = this.configService;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async joinUser(invite: PersistentEventBase, userId: UserID) {
const configService = this.configService;
const stateService = this.stateService;
const federationService = this.federationService;
const roomId = invite.roomId;
// where the room is hosted at
const residentServer = extractDomainFromId(roomId);
const residentServer = extractDomainFromId(invite.sender);
async joinUser(invite: PersistentEventBase, userId: UserID) {
// Validate that the invite is a proper invite event
if (invite.type !== 'm.room.member') {
throw new HttpException(
'Invalid invite: expected m.room.member event',
HttpStatus.BAD_REQUEST,
);
}
if (invite.content.membership !== 'invite') {
throw new HttpException(
'Invalid invite: expected membership to be invite',
HttpStatus.BAD_REQUEST,
);
}
if (invite.stateKey !== userId) {
throw new HttpException(
'Invalid invite: userId does not match invite state_key',
HttpStatus.BAD_REQUEST,
);
}
const configService = this.configService;
const stateService = this.stateService;
const federationService = this.federationService;
const roomId = invite.roomId;
// where the room is hosted at
const residentServer = extractDomainFromId(invite.sender);
🤖 Prompt for AI Agents
In packages/federation-sdk/src/services/room.service.ts around lines 787 to 794,
the joinUser method does not validate the invite parameter or ensure the target
user matches the invite; add checks to verify invite.type === 'm.room.member',
invite.content?.membership === 'invite', and that the provided userId equals
invite.stateKey. If any check fails, throw or return an early error (e.g., throw
new Error or return a rejected promise) with a clear message indicating which
validation failed; perform these validations before extracting residentServer or
proceeding with any join logic.

@sampaiodiego sampaiodiego merged commit 8a7f098 into main Oct 16, 2025
3 checks passed
@sampaiodiego sampaiodiego deleted the fix/add-users-from-same-server-on-remote-rooms branch October 16, 2025 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants