Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f8ab99a
adds homeserver synlink
ricardogarim Jul 1, 2025
552a233
creates federation-service app
ricardogarim Jul 1, 2025
b45e1e9
creates federation-matrix package
ricardogarim Jul 1, 2025
b29acfd
creates monolith API for federation
ricardogarim Jul 1, 2025
4ebe2ab
adds federation service settings
ricardogarim Jul 1, 2025
3888415
adds create federated room support
ricardogarim Jul 1, 2025
6afeb60
adds send message support
ricardogarim Jul 1, 2025
b52dac2
send message
ricardogarim Jul 2, 2025
742070d
feat: basic support for accept invitation from remote
Jul 1, 2025
c66f67f
x
ricardogarim Jul 3, 2025
c825f20
chore: replace package (#36352)
ggazzo Jul 7, 2025
c29e660
refactor: use self contained DI package from federation-sdk (#36379)
Jul 8, 2025
015ea0c
chore: add federation routes to RC (monolith + service) (#36384)
Jul 9, 2025
df2dafc
fix: avoid to register routes every time the fn gets called (#36411)
Jul 15, 2025
0a386a8
chore: new federation callbacks listeners (#36421)
Jul 15, 2025
403cb8c
fix: provide the correct emitter instance
Jul 16, 2025
86f6ac5
feat: adds license validation to federation packages and services (#3…
ricardogarim Jul 21, 2025
078fa55
chore: federation messages using new state resolution (#36548)
ricardogarim Jul 28, 2025
cc24ef3
refactor: provide env vars to federation sdk instead of loading them …
Jul 29, 2025
64ec2bb
chore: support for the server key via settings (#36551)
Jul 31, 2025
334ab57
feat: federation message reactions (#36420)
ricardogarim Jul 31, 2025
51e1c99
feat: support for inviting users using the add members tab (#36447)
Aug 1, 2025
3f3c22f
feat: adds federation kick and leave support (#36572)
ricardogarim Aug 3, 2025
5bd1969
chore: remove unused var (fix lint)
Aug 4, 2025
2e54712
feat: support for redact message from RC to external (#36492)
Aug 4, 2025
bdae1e6
feat: federation edus (typing, presence) support (#36626)
Aug 5, 2025
d764574
feat: federation threads messaging (#36624)
ricardogarim Aug 5, 2025
6301344
fix: messages redaction echo (#36633)
ricardogarim Aug 6, 2025
7f3d80d
feat: edit message (#36619)
Aug 6, 2025
7e88919
fix: use the same username pattern for federated users (#36641)
Aug 6, 2025
da37cef
feat: support for mentions and quotes on federation (#36669)
Aug 12, 2025
c305b7b
feat: name and topic change (#36700)
debdutdeb Aug 13, 2025
67b1b84
feat: auto join rooms on invite (#36729)
debdutdeb Aug 18, 2025
4ae076d
feat: change room roles (#36730)
debdutdeb Aug 18, 2025
56148aa
rebuild yarn.lock
debdutdeb Aug 18, 2025
9bb8c6f
clone and build homeserver repo
debdutdeb Aug 18, 2025
73b032f
indentatioin
debdutdeb Aug 18, 2025
6566910
change setup-node order
debdutdeb Aug 18, 2025
bb0bfce
remove existing homeserver path
debdutdeb Aug 18, 2025
882bc0e
test
debdutdeb Aug 18, 2025
7f58c78
/tmp
debdutdeb Aug 18, 2025
1576fa3
diff job :/
debdutdeb Aug 18, 2025
38f20db
must link
debdutdeb Aug 18, 2025
6ebee24
use bundle branch
debdutdeb Aug 20, 2025
074964e
fix storybook too
debdutdeb Aug 20, 2025
d5dd20d
fix lint error
debdutdeb Aug 20, 2025
a2ad5e9
fix old federation tests
debdutdeb Aug 20, 2025
4aeb36f
fix more similar issues
debdutdeb Aug 20, 2025
68ac152
Merge branch 'develop' into feat/federation
debdutdeb Aug 20, 2025
a98df67
feat: send dm
Aug 20, 2025
1b5907a
feat: receive DM
Aug 20, 2025
5639c74
fix: verify also for new federation setting
Aug 27, 2025
fee110d
Merge branch 'feat/federation' into feat/federation-dm
debdutdeb Sep 5, 2025
3a6f225
restore ci file
debdutdeb Sep 5, 2025
40e0c91
actually fix things now
debdutdeb Sep 5, 2025
fde4a47
lint
debdutdeb Sep 5, 2025
8925c71
Merge branch 'feat/federation' into feat/federation-dm
rodrigok Sep 15, 2025
21dac58
chore: removes optional from ensureFederatedUsersExistLocally method
ricardogarim Sep 16, 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
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/functions/createDirectRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function createDirectRoom(
const membersUsernames: string[] = members
.map((member) => {
if (typeof member === 'string') {
return member.replace('@', '');
return member;
}
return member.username;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ const MessageBox = ({

const { autoGrowRef, textAreaStyle } = useAutoGrow(textareaRef, isRecordingAudio);

const federationMatrixEnabled = useSetting('Federation_Matrix_enabled', false);
const serviceFederationEnabled = useSetting('Federation_Service_Enabled', false);
const matrixFederationEnabled = useSetting('Federation_Matrix_enabled') === true;
const serviceFederationEnabled = useSetting('Federation_Service_Enabled') === true;
const federationEnabled = matrixFederationEnabled || serviceFederationEnabled;

const canSend = useReactiveValue(
useCallback(() => {
if (!room.t) {
Expand All @@ -292,10 +294,10 @@ const MessageBox = ({
}

if (isRoomFederated(room)) {
return federationMatrixEnabled || serviceFederationEnabled;
return federationEnabled;
}
return true;
}, [federationMatrixEnabled, serviceFederationEnabled, room]),
}, [federationEnabled, room]),
);

const sizes = useContentBoxSize(textareaRef);
Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,28 @@ callbacks.add(
'federation-matrix-before-change-room-role',
);

callbacks.add(
'beforeCreateDirectRoom',
async (members: IUser[] | string[]): Promise<void> => {
await FederationMatrix.ensureFederatedUsersExistLocally(members);
},
callbacks.priority.HIGH,
'federation-matrix-before-create-direct-room',
);

callbacks.add(
'afterCreateDirectRoom',
async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }): Promise<void> => {
if (!room || !params || !params.creatorId || !params.creatorId) {
return;
}

await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId);
},
callbacks.priority.HIGH,
'federation-matrix-after-create-direct-room',
);

export const setupTypingEventListenerForRoom = (roomId: string): void => {
notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => {
if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) {
Expand Down
160 changes: 156 additions & 4 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,164 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS

this.logger.debug('Room creation completed successfully', room._id);
} catch (error) {
console.log(error);
this.logger.error('Failed to create room:', error);
throw error;
}
}

async ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise<void> {
try {
this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length });

for await (const member of members) {
let username: string;

if (typeof member === 'string') {
username = member;
} else {
username = member.username as string;
}

if (!username.includes(':') && !username.includes('@')) {
continue;
}

const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`;

const existingUser = await Users.findOneByUsername(username);
if (existingUser) {
const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id);
if (!existingBridge) {
const remoteDomain = externalUserId.split(':')[1] || this.serverName;
await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain);
}
continue;
}

this.logger.debug('Creating federated user locally', { externalUserId, username });

const remoteDomain = externalUserId.split(':')[1] || this.serverName;
const localName = username.split(':')[0]?.replace('@', '') || username;

const newUser = {
username,
name: localName,
type: 'user' as const,
status: UserStatus.OFFLINE,
active: true,
roles: ['user'],
requirePasswordChange: false,
federated: true,
createdAt: new Date(),
_updatedAt: new Date(),
};

const { insertedId } = await Users.insertOne(newUser);
await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, remoteDomain);

this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId });
}
} catch (error) {
this.logger.error('Failed to ensure federated users exist locally:', error);
}
}
Comment on lines +239 to +294
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for federated users provisioning.

The method doesn't validate the input array or handle edge cases like empty members list.

Add validation at the beginning of the method:

 async ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise<void> {
     try {
+        if (!members || members.length === 0) {
+            this.logger.debug('No members to provision');
+            return;
+        }
+        
         this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length });
📝 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 ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise<void> {
try {
this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length });
for await (const member of members) {
let username: string;
if (typeof member === 'string') {
username = member;
} else {
username = member.username as string;
}
if (!username.includes(':') && !username.includes('@')) {
continue;
}
const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`;
const existingUser = await Users.findOneByUsername(username);
if (existingUser) {
const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id);
if (!existingBridge) {
const remoteDomain = externalUserId.split(':')[1] || this.serverName;
await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain);
}
continue;
}
this.logger.debug('Creating federated user locally', { externalUserId, username });
const remoteDomain = externalUserId.split(':')[1] || this.serverName;
const localName = username.split(':')[0]?.replace('@', '') || username;
const newUser = {
username,
name: localName,
type: 'user' as const,
status: UserStatus.OFFLINE,
active: true,
roles: ['user'],
requirePasswordChange: false,
federated: true,
createdAt: new Date(),
_updatedAt: new Date(),
};
const { insertedId } = await Users.insertOne(newUser);
await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, remoteDomain);
this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId });
}
} catch (error) {
this.logger.error('Failed to ensure federated users exist locally:', error);
}
}
async ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise<void> {
try {
if (!members || members.length === 0) {
this.logger.debug('No members to provision');
return;
}
this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length });
for await (const member of members) {
let username: string;
if (typeof member === 'string') {
username = member;
} else {
username = member.username as string;
}
if (!username.includes(':') && !username.includes('@')) {
continue;
}
const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`;
const existingUser = await Users.findOneByUsername(username);
if (existingUser) {
const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id);
if (!existingBridge) {
const remoteDomain = externalUserId.split(':')[1] || this.serverName;
await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain);
}
continue;
}
this.logger.debug('Creating federated user locally', { externalUserId, username });
const remoteDomain = externalUserId.split(':')[1] || this.serverName;
const localName = username.split(':')[0]?.replace('@', '') || username;
const newUser = {
username,
name: localName,
type: 'user' as const,
status: UserStatus.OFFLINE,
active: true,
roles: ['user'],
requirePasswordChange: false,
federated: true,
createdAt: new Date(),
_updatedAt: new Date(),
};
const { insertedId } = await Users.insertOne(newUser);
await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, remoteDomain);
this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId });
}
} catch (error) {
this.logger.error('Failed to ensure federated users exist locally:', error);
}
}
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/FederationMatrix.ts around lines 239 to
294, the method ensureFederatedUsersExistLocally lacks input validation and
edge-case handling; add a guard at the start to verify members is an array and
return early (or log and return) if it's empty, null, or undefined; validate
each element before processing (ensure each is a non-empty string or an IUser
with a valid username string), skip and log any invalid entries rather than
throwing, and make sure any unexpected input types are handled gracefully so the
loop only attempts to process well-formed usernames.


async createDirectMessageRoom(room: IRoom, members: (IUser | string)[], creatorId: IUser['_id']): Promise<void> {
try {
this.logger.debug('Creating direct message room in Matrix', { roomId: room._id, memberCount: members.length });

if (!this.homeserverServices) {
this.logger.warn('Homeserver services not available, skipping DM room creation');
return;
}

const creator = await Users.findOneById(creatorId);
if (!creator) {
throw new Error('Creator not found in members list');
}

const matrixUserId = `@${creator.username}:${this.serverName}`;
const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(creator._id);
if (!existingMatrixUserId) {
await MatrixBridgedUser.createOrUpdateByLocalId(creator._id, matrixUserId, true, this.serverName);
}

const actualMatrixUserId = existingMatrixUserId || matrixUserId;

let matrixRoomResult: { room_id: string; event_id?: string };
if (members.length === 2) {
const otherMember = members.find((member) => {
if (typeof member === 'string') {
return true; // Remote user
}
return member._id !== creatorId;
});
if (!otherMember) {
throw new Error('Other member not found for 1-on-1 DM');
}
let otherMemberMatrixId: string;
if (typeof otherMember === 'string') {
otherMemberMatrixId = otherMember.startsWith('@') ? otherMember : `@${otherMember}`;
} else if (otherMember.username?.includes(':')) {
otherMemberMatrixId = otherMember.username.startsWith('@') ? otherMember.username : `@${otherMember.username}`;
} else {
otherMemberMatrixId = `@${otherMember.username}:${this.serverName}`;
}
const roomId = await this.homeserverServices.room.createDirectMessageRoom(actualMatrixUserId, otherMemberMatrixId);
matrixRoomResult = { room_id: roomId };
} else {
// For group DMs (more than 2 members), create a private room
const roomName = room.name || room.fname || `Group chat with ${members.length} members`;
matrixRoomResult = await this.homeserverServices.room.createRoom(actualMatrixUserId, roomName, 'invite');
}

const mapping = await MatrixBridgedRoom.getLocalRoomId(matrixRoomResult.room_id);
if (!mapping) {
await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, this.serverName);
}

for await (const member of members) {
if (typeof member !== 'string' && member._id === creatorId) continue;

try {
let memberMatrixUserId: string;
let memberId: string | undefined;

if (typeof member === 'string') {
memberMatrixUserId = member.startsWith('@') ? member : `@${member}`;
memberId = undefined;
} else if (member.username?.includes(':')) {
memberMatrixUserId = member.username.startsWith('@') ? member.username : `@${member.username}`;
memberId = member._id;
} else {
memberMatrixUserId = `@${member.username}:${this.serverName}`;
memberId = member._id;
}

if (memberId) {
const existingMemberMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(memberId);

if (!existingMemberMatrixUserId) {
await MatrixBridgedUser.createOrUpdateByLocalId(memberId, memberMatrixUserId, true, this.serverName);
}
}

if (members.length > 2) {
await this.homeserverServices.invite.inviteUserToRoom(memberMatrixUserId, matrixRoomResult.room_id, actualMatrixUserId);
}
} catch (error) {
this.logger.error('Error creating or updating bridged user for DM:', error);
}
}
await Rooms.setAsFederated(room._id);
this.logger.debug('Direct message room creation completed successfully', room._id);
} catch (error) {
this.logger.error('Failed to create direct message room:', error);
throw error;
}
}
Comment on lines +296 to +389
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify Matrix user ID format handling in DM creation.

The method constructs Matrix user IDs differently for local vs. federated users, but doesn't validate the format consistently.

Let's verify the Matrix ID format handling across the codebase:


🏁 Script executed:

#!/bin/bash
# Check how Matrix user IDs are constructed elsewhere
rg -n "startsWith\('@'\)" ee/packages/federation-matrix/src/ -A 2 -B 2

# Check for any existing validation of Matrix ID format
rg -n "@.*:.*" ee/packages/federation-matrix/src/ --type ts | grep -E "(validate|check|verify)"

# Look for other instances of Matrix ID construction
ast-grep --pattern 'username.startsWith("@") ? username : `@${username}`'

Length of output: 1638


Normalize and validate Matrix user IDs before use

  • Inconsistent handling: string members are only prefixed with '@' (producing '@alice' without a domain) while object members get :${this.serverName} appended when username lacks ':'. This can produce invalid MXIDs and break homeserver API calls.
  • Fix: normalize every MXID to the canonical @localpart:domain before calling homeserverServices (ensure leading '@' and, if no ':' present, append :${this.serverName}). Implement or reuse a single normalize/validate helper and call it for both string and object branches; if validation fails, log and skip that member instead of throwing.
  • Location: ee/packages/federation-matrix/src/FederationMatrix.ts — around lines 329–336 and 356–363.


async sendMessage(message: IMessage, room: IRoom, user: IUser): Promise<void> {
try {
const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id);
Expand All @@ -260,7 +413,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
const parsedMessage = await toExternalMessageFormat({
message: message.msg,
externalRoomId: matrixRoomId,
homeServerDomain: await this.serverName,
homeServerDomain: this.serverName,
});
if (!message.tmid) {
if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
Expand Down Expand Up @@ -370,7 +523,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
return;
}
const messageToReplyTo = await Messages.findOneById(messageToReplyToId);
if (!messageToReplyTo || !messageToReplyTo.federation?.eventId) {
if (!messageToReplyTo?.federation?.eventId) {
return;
}

Expand Down Expand Up @@ -679,7 +832,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
const parsedMessage = await toExternalMessageFormat({
message: newContent,
externalRoomId: matrixRoomId,
homeServerDomain: await this.serverName,
homeServerDomain: this.serverName,
});
const eventId = await this.homeserverServices.message.updateMessage(
matrixRoomId,
Expand Down Expand Up @@ -771,7 +924,6 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
} else if (role === 'moderator') {
powerLevel = 50;
}

await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel);
}
}
Loading
Loading