diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index 6b707e74..f42c4db5 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -1,5 +1,5 @@ import type { EventStore } from '@rocket.chat/federation-core'; -import type { PduForType, PduType } from '@rocket.chat/federation-room'; +import type { PduForType, PduType, UserID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { AppConfig, ConfigService } from './services/config.service'; @@ -43,12 +43,28 @@ export class FederationSDK { private readonly federationValidationService: FederationValidationService, ) {} + /** + * @deprecated use createDirectMessage instead + */ createDirectMessageRoom( ...args: Parameters ) { return this.roomService.createDirectMessageRoom(...args); } + async createDirectMessage({ + creatorUserId, + members, + }: { + creatorUserId: UserID; + members: UserID[]; + }) { + return this.roomService.createDirectMessage({ + creatorUserId, + members, + }); + } + createRoom(...args: Parameters) { return this.roomService.createRoom(...args); } @@ -153,13 +169,6 @@ export class FederationSDK { return this.eventAuthorizationService.verifyRequestSignature(...args); } - /** - * @deprecated - */ - joinUser(...args: Parameters) { - return this.roomService.joinUser(...args); - } - acceptInvite(...args: Parameters) { return this.roomService.acceptInvite(...args); } diff --git a/packages/federation-sdk/src/services/invite.service.ts b/packages/federation-sdk/src/services/invite.service.ts index 2ecf9030..9a2a5aff 100644 --- a/packages/federation-sdk/src/services/invite.service.ts +++ b/packages/federation-sdk/src/services/invite.service.ts @@ -217,6 +217,11 @@ export class InviteService { await this.stateService.handlePdu(inviteEvent); + await this.emitterService.emit('homeserver.matrix.membership', { + event_id: inviteEvent.eventId, + event: inviteEvent.event, + }); + return inviteEvent; } diff --git a/packages/federation-sdk/src/services/room.service.spec.ts b/packages/federation-sdk/src/services/room.service.spec.ts index 207fcc47..8b94f2e8 100644 --- a/packages/federation-sdk/src/services/room.service.spec.ts +++ b/packages/federation-sdk/src/services/room.service.spec.ts @@ -226,7 +226,7 @@ describe('RoomService', async () => { expect(imtialStateKeys).toEqual(expectedStateKeys); - await roomService.joinUser(roomId, username, secondaryUsername); + await roomService.joinUser(roomId, secondaryUsername); const state = await stateService.getLatestRoomState(roomId); diff --git a/packages/federation-sdk/src/services/room.service.ts b/packages/federation-sdk/src/services/room.service.ts index a34d1212..cdc32066 100644 --- a/packages/federation-sdk/src/services/room.service.ts +++ b/packages/federation-sdk/src/services/room.service.ts @@ -910,13 +910,13 @@ export class RoomService { // if local room, add the user to the room if allowed. // if remote room, run through the join process - async joinUser(roomId: RoomID, sender: UserID, userId: UserID) { + async joinUser(roomId: RoomID, userId: UserID) { const configService = this.configService; const stateService = this.stateService; const federationService = this.federationService; // where the room is hosted at - const residentServer = extractDomainFromId(sender); + const residentServer = extractDomainFromId(roomId); // our own room, we can validate the join event by ourselves // once done, emit the event to all participating servers @@ -1130,7 +1130,7 @@ export class RoomService { ); } - return this.joinUser(roomId, inviteEventStore.event.sender, userId); + return this.joinUser(roomId, userId); } async rejectInvite(roomId: RoomID, userId: UserID): Promise { @@ -1500,6 +1500,198 @@ export class RoomService { void this.federationService.sendEventToAllServersInRoom(event); } + async createDirectMessage({ + creatorUserId, + members, + }: { + creatorUserId: UserID; + members: UserID[]; + }) { + const roomCreateEvent = + PersistentEventFactory.newCreateEvent(creatorUserId); + + await this.stateService.signEvent(roomCreateEvent); + + await this.stateService.handlePdu(roomCreateEvent); + + const creatorMembershipEvent = + await this.stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + auth_events: [], + prev_events: [], + sender: creatorUserId, + content: { + membership: 'join', + is_direct: true, + displayname: creatorUserId.split(':').shift()?.slice(1), + }, + depth: 2, + room_id: roomCreateEvent.roomId, + state_key: creatorUserId, + origin_server_ts: Date.now(), + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(creatorMembershipEvent); + + const powerLevelsEvent = + await this.stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + auth_events: [], + prev_events: [], + content: { + users: { + [creatorUserId]: 100, + ...(members.length === 1 ? { [members[0]]: 100 } : {}), // 1:1 DM both get 100 power level + }, + users_default: 0, + events: { + 'm.room.name': 50, + 'm.room.power_levels': 100, + 'm.room.history_visibility': 100, + 'm.room.canonical_alias': 50, + 'm.room.avatar': 50, + 'm.room.tombstone': 100, + 'm.room.server_acl': 100, + 'm.room.encryption': 100, + }, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 0, + // historical: 100, + }, + room_id: roomCreateEvent.roomId, + state_key: '', + depth: 3, + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(powerLevelsEvent); + + const joinRulesEvent = + await this.stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + auth_events: [], + prev_events: [], + content: { join_rule: 'invite' }, + room_id: roomCreateEvent.roomId, + state_key: '', + depth: 4, + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(joinRulesEvent); + + const historyVisibilityEvent = + await this.stateService.buildEvent<'m.room.history_visibility'>( + { + type: 'm.room.history_visibility', + content: { history_visibility: 'shared' }, + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 5, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(historyVisibilityEvent); + + const guestAccessEvent = + await this.stateService.buildEvent<'m.room.guest_access'>( + { + type: 'm.room.guest_access', + content: { guest_access: 'forbidden' }, // synapse uses 'can_join' for DMs + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 6, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(guestAccessEvent); + + let memberDepthCounter = 7; + + for await (const member of members) { + const targetMembershipEvent = + await this.stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + auth_events: [], + prev_events: [], + content: { + membership: 'invite', + displayname: member.split(':').shift()?.slice(1), + ...(members.length === 1 && { is_direct: true }), // synapse don't send is_direct on invites for group DMs + }, + room_id: roomCreateEvent.roomId, + state_key: member, + depth: memberDepthCounter++, + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + const targetServerName = extractDomainFromId(member); + + if (targetServerName !== this.configService.serverName) { + // TODO this may not be the best place to do this validation + await this.federationValidationService.validateOutboundInvite( + member, + roomCreateEvent.roomId, + ); + + // get signed invite event + const inviteResponse = await this.federationService.inviteUser( + targetMembershipEvent, + roomCreateEvent.version, + ); + + // try to save + // can only invite if already part of the room + await this.stateService.handlePdu( + PersistentEventFactory.createFromRawEvent( + inviteResponse.event, + roomCreateEvent.version, + ), + ); + + // void this.federationService.sendEventToAllServersInRoom( + // targetMembershipEvent, + // ); + } else { + await this.stateService.handlePdu(targetMembershipEvent); + } + } + + return roomCreateEvent.roomId; + } + + /** + * @deprecated Use createDirectMessage instead + */ async createDirectMessageRoom( creatorUserId: UserID, targetUserId: UserID, diff --git a/packages/room/src/manager/factory.ts b/packages/room/src/manager/factory.ts index 216bdf7f..b78098b6 100644 --- a/packages/room/src/manager/factory.ts +++ b/packages/room/src/manager/factory.ts @@ -82,7 +82,10 @@ export class PersistentEventFactory { // create individual events // a m.room.create event, adds the roomId too - static newCreateEvent(creator: UserID, roomVersion: RoomVersion) { + static newCreateEvent( + creator: UserID, + roomVersion: RoomVersion = PersistentEventFactory.defaultRoomVersion, + ) { if (!PersistentEventFactory.isSupportedRoomVersion(roomVersion)) { throw new Error(`Room version ${roomVersion} is not supported`); }