Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions packages/federation-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Room } from './repositories/room.repository';
import { Server } from './repositories/server.repository';
import { StateGraphStore } from './repositories/state-graph.repository';
import { Upload } from './repositories/upload.repository';
import { User } from './repositories/user.repository';
import { FederationSDK } from './sdk';
import { DatabaseConnectionService } from './services/database-connection.service';
import { EventEmitterService } from './services/event-emitter.service';
Expand Down Expand Up @@ -223,7 +224,18 @@ export type HomeserverEventSignatures = {
displayname?: string;
avatar_url?: string;
reason?: string;
is_direct?: boolean;
};
stripped_state?: PduForType<
| 'm.room.create'
| 'm.room.name'
| 'm.room.avatar'
| 'm.room.topic'
| 'm.room.join_rules'
| 'm.room.canonical_alias'
| 'm.room.encryption'
| 'm.room.member'
>[];
};
'homeserver.matrix.room.name': {
event_id: EventID;
Expand Down Expand Up @@ -312,6 +324,10 @@ export async function init({
),
});

container.register<Collection<User>>('UserCollection', {
useValue: db.collection<User>('users'),
});

const eventEmitterService = container.resolve(EventEmitterService);
if (emitter) {
eventEmitterService.setEmitter(emitter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export class EventStagingRepository {
return this.collection.updateOne(
{
_id: eventId,
origin,
},
{
$setOnInsert: {
Expand All @@ -38,6 +37,7 @@ export class EventStagingRepository {
got: 0,
},
$set: {
origin,
event,
from,
},
Expand Down
23 changes: 23 additions & 0 deletions packages/federation-sdk/src/repositories/event.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,29 @@ export class EventRepository {
);
}

forceInsertOrUpdateEventWithStateId(
eventId: EventID,
event: Pdu,
stateId: StateID,
partial = false,
): Promise<UpdateResult> {
return this.collection.updateOne(
{ _id: eventId },
{
$setOnInsert: {
nextEventId: '',
createdAt: new Date(),
},
$set: {
event,
stateId,
partial,
},
},
{ upsert: true },
);
}

async updateNextEventReferences(
newEventId: EventID,
previousEventIds: EventID[],
Expand Down
48 changes: 48 additions & 0 deletions packages/federation-sdk/src/repositories/user.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Collection } from 'mongodb';
import { inject, singleton } from 'tsyringe';

export type User = {
_id: string;
username?: string;
name?: string;
avatarUrl?: string;
avatarETag?: string;
federated?: boolean;
federation?: {
version?: number;
mui?: string;
origin?: string;
avatarUrl?: string;
};
createdAt: Date;
_updatedAt: Date;
};

@singleton()
export class UserRepository {
constructor(
@inject('UserCollection') private readonly collection: Collection<User>,
) {}

async findByUsername(username: string): Promise<User | null> {
return this.collection.findOne(
{
username,
$or: [{ federated: { $exists: false } }, { federated: false }],
},
{
projection: {
_id: 1,
username: 1,
name: 1,
avatarUrl: 1,
avatarETag: 1,
federation: 1,
federated: 1,
createdAt: 1,
_updatedAt: 1,
},
},
);
}
}
26 changes: 26 additions & 0 deletions packages/federation-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { singleton } from 'tsyringe';
import { AppConfig, ConfigService } from './services/config.service';
import { EduService } from './services/edu.service';
import { EventAuthorizationService } from './services/event-authorization.service';
import { EventEmitterService } from './services/event-emitter.service';
import { EventService } from './services/event.service';
import { FederationRequestService } from './services/federation-request.service';
import { FederationService } from './services/federation.service';
Expand Down Expand Up @@ -37,6 +38,7 @@ export class FederationSDK {
private readonly wellKnownService: WellKnownService,
private readonly federationRequestService: FederationRequestService,
private readonly federationService: FederationService,
private readonly eventEmitterService: EventEmitterService,
) {}

createDirectMessageRoom(
Expand Down Expand Up @@ -99,6 +101,12 @@ export class FederationSDK {
return this.messageService.updateMessage(...args);
}

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

updateRoomName(...args: Parameters<typeof this.roomService.updateRoomName>) {
return this.roomService.updateRoomName(...args);
}
Expand Down Expand Up @@ -145,6 +153,20 @@ export class FederationSDK {
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);
}

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

getLatestRoomState2(
...args: Parameters<typeof this.stateService.getLatestRoomState2>
) {
Expand Down Expand Up @@ -293,4 +315,8 @@ export class FederationSDK {
) {
return this.eduService.sendPresenceUpdateToRooms(...args);
}

emit(...args: Parameters<typeof this.eventEmitterService.emit>) {
return this.eventEmitterService.emit(...args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ export class EventAuthorizationService {
}

if (entityType === 'media') {
// avatars are publicly accessible and don't require access control
if (entityId.startsWith('avatar')) {
// TODO: add avatar access control once we have a way to check if the user is allowed to access the avatar
return true;
}

return this.canAccessMedia(entityId, serverName);
}

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
62 changes: 59 additions & 3 deletions packages/federation-sdk/src/services/invite.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
PersistentEventFactory,
RoomID,
RoomVersion,
StateID,
UserID,
extractDomainFromId,
} from '@rocket.chat/federation-room';
import { singleton } from 'tsyringe';
import { delay, inject, singleton } from 'tsyringe';
import { EventRepository } from '../repositories/event.repository';
import { UserRepository } from '../repositories/user.repository';
import { ConfigService } from './config.service';
import { EventAuthorizationService } from './event-authorization.service';
import { EventEmitterService } from './event-emitter.service';
import { FederationService } from './federation.service';
import { StateService, UnknownRoomError } from './state.service';
import { StateService } from './state.service';
// TODO: Have better (detailed/specific) event input type
export type ProcessInviteEvent = {
event: EventBase;
Expand All @@ -37,8 +41,38 @@ export class InviteService {
private readonly stateService: StateService,
private readonly configService: ConfigService,
private readonly eventAuthorizationService: EventAuthorizationService,
@inject(delay(() => UserRepository))
private readonly userRepository: UserRepository,
private readonly eventEmitterService: EventEmitterService,
@inject(delay(() => EventRepository))
private readonly eventRepository: EventRepository,
) {}

/**
* Get avatar URL for a user (local users only)
*/
private async getAvatarUrlForUser(
userId: UserID,
): Promise<string | undefined> {
const userDomain = extractDomainFromId(userId);
const localDomain = this.configService.serverName;

if (userDomain !== localDomain) {
return undefined;
}

const username = userId.split(':')[0]?.slice(1);
if (!username) {
return undefined;
}

// Fetch user to get avatarETag
const user = await this.userRepository.findByUsername(username);
const avatarIdentifier = user?.avatarETag || username;

return `mxc://${localDomain}/avatar${avatarIdentifier}`;
}

/**
* Invite a user to an existing room
*/
Expand All @@ -64,6 +98,8 @@ export class InviteService {
? userId.split(':').shift()?.slice(1)
: undefined;

const avatarUrl = await this.getAvatarUrlForUser(userId);

const inviteEvent = await stateService.buildEvent<'m.room.member'>(
{
type: 'm.room.member',
Expand All @@ -73,6 +109,7 @@ export class InviteService {
is_direct: true,
displayname: displayname,
}),
...(avatarUrl && { avatar_url: avatarUrl }),
},
room_id: roomId,
state_key: userId,
Expand Down Expand Up @@ -152,6 +189,7 @@ export class InviteService {
| 'm.room.join_rules'
| 'm.room.canonical_alias'
| 'm.room.encryption'
| 'm.room.member'
>[],
): Promise<void> {
const isRoomNonPrivate = strippedStateEvents.some(
Expand Down Expand Up @@ -191,6 +229,7 @@ export class InviteService {
| 'm.room.join_rules'
| 'm.room.canonical_alias'
| 'm.room.encryption'
| 'm.room.member'
>[],
) {
// 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
Expand Down Expand Up @@ -221,13 +260,30 @@ export class InviteService {
);

// 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
} else {
await this.eventRepository.forceInsertOrUpdateEventWithStateId(
inviteEvent.eventId,
inviteEvent.event,
'' as StateID,
true, // partial = true
);
}

this.eventEmitterService.emit('homeserver.matrix.membership', {
event_id: inviteEvent.eventId,
event: inviteEvent.event,
room_id: roomId,
sender: inviteEvent.sender,
state_key: inviteEvent.stateKey ?? '',
origin_server_ts: inviteEvent.originServerTs,
content: inviteEvent.getContent(),
stripped_state: strippedStateEvents,
});

// 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