Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
598bcc5
feat: add forceInsertOrUpdateEventWithStateId and emit membership events
ricardogarim Nov 17, 2025
9a81126
feat: add invite acceptance handlers
ricardogarim Nov 17, 2025
5b15bd8
refactor: make accept and reject invite methods to receive room and u…
ricardogarim Nov 24, 2025
e43d2a2
fix: add stripped events validation on invite processing
ricardogarim Nov 25, 2025
c745863
chore: early checks and bring handlePdu back
ricardogarim Nov 25, 2025
7bfdd8b
chore: rename forceInsertOrUpdateEventWithStateId to insertInviteEvent
ricardogarim Nov 25, 2025
1baa731
chore: bring handlePdu back to processInvite - rename later
ricardogarim Nov 26, 2025
b32de25
fix: adjust emitter calls on accept, reject and leave functions
ricardogarim Nov 27, 2025
72078f8
refactor: get room version when accepting invite from stripped events
ricardogarim Nov 27, 2025
4be567c
test: add accept and reject invite unit tests
ricardogarim Nov 27, 2025
26ba04c
fix: prevent signing invites for users on other servers
ricardogarim Nov 28, 2025
aeada7d
chore: add stripped events to processInvite input
ricardogarim Nov 28, 2025
dff0ac4
chore: save received invite from remote as outlier
ricardogarim Nov 28, 2025
a23cb51
save invite on state
sampaiodiego Nov 28, 2025
5e121d1
feat: add make leave support
ricardogarim Dec 1, 2025
23afd67
feat: add sendLeave federationSDK method and rules support
ricardogarim Dec 1, 2025
4dee9a7
refactor: remove unneeded event emitter cals
ricardogarim Dec 1, 2025
fe961c3
chore: remove joinUser method from SDK export
ricardogarim Dec 1, 2025
9bece91
chore: joinUser now receives only roomId and sender
sampaiodiego Dec 1, 2025
0cc40a0
do not validate strippedStateEvents
sampaiodiego Dec 1, 2025
8ec454a
get residentServer from event
sampaiodiego Dec 1, 2025
03e1398
revert auth-rule check for leave
sampaiodiego Dec 1, 2025
192a6f7
fix lint
sampaiodiego Dec 1, 2025
da7e3a8
chore: save invite stripped events as unsigned.invite_room_state
ricardogarim Dec 2, 2025
59ae9ac
add joinUser back
sampaiodiego Dec 2, 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
18 changes: 18 additions & 0 deletions packages/federation-sdk/src/repositories/event.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Collection,
FindCursor,
FindOptions,
InsertOneResult,
UpdateResult,
WithId,
} from 'mongodb';
Expand Down Expand Up @@ -405,6 +406,23 @@ export class EventRepository {
);
}

insertOutlierEvent(
eventId: EventID,
event: Pdu,
origin: string,
): Promise<InsertOneResult<EventStore>> {
return this.collection.insertOne({
_id: eventId,
event,
createdAt: new Date(),
origin,
stateId: '' as StateID,
nextEventId: '',
outlier: true,
partial: false,
});
}

async updateNextEventReferences(
newEventId: EventID,
previousEventIds: EventID[],
Expand Down
19 changes: 19 additions & 0 deletions packages/federation-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ export class FederationSDK {
return this.eventService.getEventById(...args);
}

makeLeave(...args: Parameters<typeof this.roomService.makeLeave>) {
return this.roomService.makeLeave(...args);
}

sendLeave(...args: Parameters<typeof this.roomService.sendLeave>) {
return this.roomService.sendLeave(...args);
}

leaveRoom(...args: Parameters<typeof this.roomService.leaveRoom>) {
return this.roomService.leaveRoom(...args);
}
Expand Down Expand Up @@ -141,10 +149,21 @@ export class FederationSDK {
return this.eventAuthorizationService.verifyRequestSignature(...args);
}

/**
* @deprecated
*/
joinUser(...args: Parameters<typeof this.roomService.joinUser>) {
return this.roomService.joinUser(...args);
}

acceptInvite(...args: Parameters<typeof this.roomService.acceptInvite>) {
return this.roomService.acceptInvite(...args);
}

rejectInvite(...args: Parameters<typeof this.roomService.rejectInvite>) {
return this.roomService.rejectInvite(...args);
}

getLatestRoomState2(
...args: Parameters<typeof this.stateService.getLatestRoomState2>
) {
Expand Down
44 changes: 44 additions & 0 deletions packages/federation-sdk/src/services/federation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,50 @@ export class FederationService {
}
}

async makeLeave(
domain: string,
roomId: string,
userId: string,
): Promise<{ event: Pdu; room_version: string }> {
try {
const uri = FederationEndpoints.makeLeave(roomId, userId);
return await this.requestService.get<{
event: Pdu;
room_version: string;
}>(domain, uri);
} catch (error: any) {
this.logger.error({ msg: 'makeLeave failed', err: error });
throw error;
}
}

async sendLeave(leaveEvent: PersistentEventBase): Promise<void> {
try {
const uri = FederationEndpoints.sendLeave(
leaveEvent.roomId,
leaveEvent.eventId,
);

const residentServer = leaveEvent.roomId.split(':').pop();

if (!residentServer) {
this.logger.debug({ msg: 'invalid room_id', event: leaveEvent.event });
throw new Error(
`invalid room_id ${leaveEvent.roomId}, no server_name part`,
);
}

await this.requestService.put<void>(
residentServer,
uri,
leaveEvent.event,
);
} catch (error: any) {
this.logger.error({ msg: 'sendLeave failed', err: error });
throw error;
}
}

/**
* Send a transaction to a remote server
*/
Expand Down
96 changes: 50 additions & 46 deletions packages/federation-sdk/src/services/invite.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventBase, createLogger } from '@rocket.chat/federation-core';
import { createLogger } from '@rocket.chat/federation-core';
import {
EventID,
PduForType,
Expand All @@ -9,24 +9,13 @@ import {
UserID,
extractDomainFromId,
} from '@rocket.chat/federation-room';
import { singleton } from 'tsyringe';
import { delay, inject, singleton } from 'tsyringe';
import { EventRepository } from '../repositories/event.repository';
import { ConfigService } from './config.service';
import { EventAuthorizationService } from './event-authorization.service';
import { EventEmitterService } from './event-emitter.service';
import { EventService } from './event.service';
import { FederationService } from './federation.service';
import {
RoomInfoNotReadyError,
StateService,
UnknownRoomError,
} from './state.service';
// TODO: Have better (detailed/specific) event input type
export type ProcessInviteEvent = {
event: EventBase;
invite_room_state: unknown;
room_version: string;
};

import { StateService } from './state.service';
export class NotAllowedError extends Error {
constructor(message: string) {
super(message);
Expand All @@ -44,6 +33,8 @@ export class InviteService {
private readonly configService: ConfigService,
private readonly eventAuthorizationService: EventAuthorizationService,
private readonly emitterService: EventEmitterService,
@inject(delay(() => EventRepository))
private readonly eventRepository: EventRepository,
) {}

/**
Expand Down Expand Up @@ -110,11 +101,6 @@ export class InviteService {
// without it join events will not be processed if /event/{eventId} causes problems
void federationService.sendEventToAllServersInRoom(inviteEvent);

this.emitterService.emit('homeserver.matrix.membership', {
event_id: inviteEvent.eventId,
event: inviteEvent.event,
});

return {
event_id: inviteEvent.eventId,
event: PersistentEventFactory.createFromRawEvent(
Expand Down Expand Up @@ -145,11 +131,6 @@ export class InviteService {
// let everyone know
void federationService.sendEventToAllServersInRoom(inviteEvent);

this.emitterService.emit('homeserver.matrix.membership', {
event_id: inviteEvent.eventId,
event: inviteEvent.event,
});

return {
event_id: inviteEvent.eventId,
event: PersistentEventFactory.createFromRawEvent(
Expand Down Expand Up @@ -196,10 +177,8 @@ export class InviteService {

async processInvite(
event: PduForType<'m.room.member'>,
roomId: RoomID,
eventId: EventID,
roomVersion: RoomVersion,
authenticatedServer: string,
strippedStateEvents: PduForType<
| 'm.room.create'
| 'm.room.name'
Expand All @@ -209,15 +188,9 @@ export class InviteService {
| 'm.room.canonical_alias'
| 'm.room.encryption'
>[],
) {
// SPEC: when a user invites another user on a different homeserver, a request to that homeserver to have the event signed and verified must be made
): Promise<PersistentEventBase<RoomVersion, 'm.room.member'>> {
await this.shouldProcessInvite(strippedStateEvents);

const residentServer = roomId.split(':').pop();
if (!residentServer) {
throw new Error(`Invalid roomId ${roomId}`);
}

const inviteEvent =
PersistentEventFactory.createFromRawEvent<'m.room.member'>(
event,
Expand All @@ -228,28 +201,59 @@ export class InviteService {
throw new Error(`Invalid eventId ${eventId}`);
}

await this.stateService.signEvent(inviteEvent);
const { residentServer } = inviteEvent;

// we are the host of the server
if (residentServer === this.configService.serverName) {
await this.eventAuthorizationService.checkAclForInvite(
roomId,
authenticatedServer,
event.room_id,
residentServer,
);

// attempt to persist the invite event as we already have the state

await this.stateService.handlePdu(inviteEvent);

// we do not send transaction here
// the asking server will handle the transactions
return inviteEvent;
}

this.emitterService.emit('homeserver.matrix.membership', {
event_id: inviteEvent.eventId,
event: inviteEvent.event,
});
const invitedServer = extractDomainFromId(event.state_key);
if (!invitedServer) {
throw new Error(
`invalid state_key ${event.state_key}, no server_name part`,
);
}
if (invitedServer !== this.configService.serverName) {
throw new Error(
`Cannot sign invite for user ${event.state_key}: user does not belong to this server (${this.configService.serverName})`,
);
}

await this.stateService.signEvent(inviteEvent);

// we have no specific structure to store the invite_room_state received on the invite route,
// so we store it in the unsigned section of the invite event.
inviteEvent.event.unsigned.invite_room_state = strippedStateEvents;

// check if we are already in the room, if so we can handlePdu because we have the state and should save
// the invite in the state as well
const createEvent = await this.eventRepository.findByRoomIdAndType(
event.room_id,
'm.room.create',
);
if (createEvent) {
await this.stateService.handlePdu(inviteEvent);
} else {
// otherwise we save as outlier only so we can deal with it later
await this.eventRepository.insertOutlierEvent(
inviteEvent.eventId,
inviteEvent.event,
residentServer,
);
}

this.emitterService.emit('homeserver.matrix.membership', {
event_id: inviteEvent.eventId,
event: inviteEvent.event,
});

// we are not the host of the server
// so being the origin of the user, we sign the event and send it to the asking server, let them handle the transactions
return inviteEvent;
Expand Down
Loading