Skip to content
2 changes: 2 additions & 0 deletions packages/core/src/models/event.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface EventStore<E = Pdu> extends PersistentEventBase<E> {
reason: string;
rejectedBy?: EventID;
};

partial: boolean;
}

export interface EventStagingStore extends PersistentEventBase {
Expand Down
11 changes: 11 additions & 0 deletions packages/federation-sdk/src/repositories/event.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PduForType,
PduType,
RejectCode,
RoomID,
StateID,
} from '@rocket.chat/federation-room';
import type {
Expand Down Expand Up @@ -391,6 +392,7 @@ export class EventRepository {
eventId: EventID,
event: Pdu,
stateId: StateID,
partial = false,
): Promise<UpdateResult> {
return this.collection.updateOne(
{ _id: eventId },
Expand All @@ -402,6 +404,7 @@ export class EventRepository {
},
$set: {
stateId,
partial,
},
},
{ upsert: true },
Expand Down Expand Up @@ -444,6 +447,7 @@ export class EventRepository {
event,
stateId,
nextEventId: '',
partial: false,
},
$set: {
rejectCode: code,
Expand All @@ -467,4 +471,11 @@ export class EventRepository {
findByType(type: PduType) {
return this.collection.find({ 'event.type': type });
}

findPartialsByRoomId(roomId: RoomID) {
return this.collection.find(
{ 'event.room_id': roomId, partial: true },
{ sort: { 'event.depth': 1, createdAt: 1 } },
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type StateGraphStore = {
depth: number;

createdAt: Date;

partial: boolean;
};

@singleton()
Expand Down Expand Up @@ -180,6 +182,8 @@ export class StateGraphRepository {
chainId = new ObjectId().toString();
}

const partial = event.isPartial() || (previousDelta?.partial ?? false);

await this.collection.insertOne({
_id: stateId,
createdAt: new Date(),
Expand All @@ -190,6 +194,7 @@ export class StateGraphRepository {
previousNode: previousStateId,
chainId,
depth,
partial,
});

return stateId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class EventFetcherService {
};
}

private async fetchEventsFromFederation(
async fetchEventsFromFederation(
eventIds: string[],
targetServerName: string,
): Promise<Pdu[]> {
Expand Down
13 changes: 10 additions & 3 deletions packages/federation-sdk/src/services/federation.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { EventBase } from '@rocket.chat/federation-core';
import type { BaseEDU } from '@rocket.chat/federation-core';
import type { ProtocolVersionKey } from '@rocket.chat/federation-core';
import { createLogger } from '@rocket.chat/federation-core';
import {
Pdu,
PduForType,
PersistentEventBase,
PersistentEventFactory,
extractDomainFromId,
} from '@rocket.chat/federation-room';
import { singleton } from 'tsyringe';
import {
Expand Down Expand Up @@ -235,7 +234,15 @@ export class FederationService {
}

async sendEventToAllServersInRoom(event: PersistentEventBase) {
const servers = await this.stateService.getServersInRoom(event.roomId);
const servers = await this.stateService.getServerSetInRoom(event.roomId);

if (event.stateKey) {
const server = extractDomainFromId(event.stateKey);
// TODO: fgetser
if (!servers.has(server)) {
servers.add(server);
}
}
Comment on lines +237 to +245
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

Guard domain extraction from state_key; remove stray TODO

extractDomainFromId throws on inputs without a colon (e.g., aliases or third-party invite tokens). Add a safe guard and drop the TODO.

-		if (event.stateKey) {
-			const server = extractDomainFromId(event.stateKey);
-			// TODO: fgetser
-			if (!servers.has(server)) {
-				servers.add(server);
-			}
-		}
+		if (event.stateKey?.includes(':')) {
+			const server = extractDomainFromId(event.stateKey);
+			if (!servers.has(server)) {
+				servers.add(server);
+			}
+		} else if (event.stateKey) {
+			this.logger.debug({ stateKey: event.stateKey }, 'state_key has no domain; skipping server augmentation');
+		}
🤖 Prompt for AI Agents
In packages/federation-sdk/src/services/federation.service.ts around lines 237
to 245, the call to extractDomainFromId on event.stateKey can throw for inputs
without a colon (aliases or third‑party invite tokens); guard this by either
checking that event.stateKey contains ':' before calling extractDomainFromId or
wrap the call in a try/catch and only add the extracted server to servers if
extraction succeeds, and remove the stray TODO comment.


for (const server of servers) {
if (server === event.origin) {
Expand Down
39 changes: 9 additions & 30 deletions packages/federation-sdk/src/services/invite.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import {
RoomID,
RoomVersion,
UserID,
extractDomainFromId,
} from '@rocket.chat/federation-room';
import { singleton } from 'tsyringe';
import { ConfigService } from './config.service';
import { EventService } from './event.service';
import { FederationService } from './federation.service';
import { StateService } from './state.service';
import { StateService, UnknownRoomError } from './state.service';
// TODO: Have better (detailed/specific) event input type
export type ProcessInviteEvent = {
event: EventBase;
Expand Down Expand Up @@ -50,7 +51,7 @@ export class InviteService {
const stateService = this.stateService;
const federationService = this.federationService;

const roomInformation = await stateService.getRoomInformation(roomId);
const roomVersion = await this.stateService.getRoomVersion(roomId);

// Extract displayname from userId for direct messages
const displayname = isDirectMessage
Expand All @@ -76,12 +77,12 @@ export class InviteService {
sender: sender,
},

roomInformation.room_version,
roomVersion,
);

// SPEC: Invites a remote user to a room. Once the event has been signed by both the inviting homeserver and the invited homeserver, it can be sent to all of the servers in the room by the inviting homeserver.

const invitedServer = inviteEvent.stateKey?.split(':').pop();
const invitedServer = extractDomainFromId(inviteEvent.stateKey ?? '');
if (!invitedServer) {
throw new Error(
`invalid state_key ${inviteEvent.stateKey}, no server_name part`,
Expand All @@ -92,10 +93,6 @@ export class InviteService {
if (invitedServer === this.configService.serverName) {
await stateService.handlePdu(inviteEvent);

if (inviteEvent.rejected) {
throw new Error(inviteEvent.rejectReason);
}

// let all servers know of this state change
// without it join events will not be processed if /event/{eventId} causes problems
void federationService.sendEventToAllServersInRoom(inviteEvent);
Expand All @@ -104,7 +101,7 @@ export class InviteService {
event_id: inviteEvent.eventId,
event: PersistentEventFactory.createFromRawEvent(
inviteEvent.event,
roomInformation.room_version,
roomVersion,
),
room_id: roomId,
};
Expand All @@ -115,15 +112,15 @@ export class InviteService {

const inviteResponse = await federationService.inviteUser(
inviteEvent,
roomInformation.room_version,
roomVersion,
);

// try to save
// can only invite if already part of the room
await stateService.handlePdu(
PersistentEventFactory.createFromRawEvent(
inviteResponse.event,
roomInformation.room_version,
roomVersion,
),
);

Expand All @@ -134,7 +131,7 @@ export class InviteService {
event_id: inviteEvent.eventId,
event: PersistentEventFactory.createFromRawEvent(
inviteEvent.event,
roomInformation.room_version,
roomVersion,
),
room_id: roomId,
};
Expand Down Expand Up @@ -171,9 +168,6 @@ export class InviteService {
// attempt to persist the invite event as we already have the state

await this.stateService.handlePdu(inviteEvent);
if (inviteEvent.rejected) {
throw new Error(inviteEvent.rejectReason);
}

// we do not send transaction here
// the asking server will handle the transactions
Expand All @@ -182,21 +176,6 @@ export class InviteService {
return inviteEvent;
}

// are we already in the room?
try {
await this.stateService.getRoomInformation(roomId);

// if we have the state we try to persist the invite event
await this.stateService.handlePdu(inviteEvent);
if (inviteEvent.rejected) {
throw new Error(inviteEvent.rejectReason);
}
} catch {
// don't have state copy yet
// console.error(e);
// typical noop, we sign and return the event, nothing to do
}

// 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
8 changes: 8 additions & 0 deletions packages/federation-sdk/src/services/profiles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export class ProfilesService {
throw new Error(`Unsupported room version: ${roomVersion}`);
}

if (
!(await this.stateService.getLatestRoomState2(roomId)).isUserInvited(
userId,
)
) {
throw new Error(`User ${userId} is not invited`);
}

const membershipEvent = await stateService.buildEvent<'m.room.member'>(
{
type: 'm.room.member',
Expand Down
Loading