Skip to content

Commit e455f15

Browse files
committed
fix: accept internal invites
1 parent b9ccf15 commit e455f15

File tree

4 files changed

+107
-51
lines changed

4 files changed

+107
-51
lines changed

ee/packages/federation-matrix/src/FederationMatrix.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
2626
import emojione from 'emojione';
2727

2828
import { getWellKnownRoutes } from './api/.well-known/server';
29-
import { getMatrixInviteRoutes } from './api/_matrix/invite';
29+
import { acceptInvite, getMatrixInviteRoutes } from './api/_matrix/invite';
3030
import { getKeyServerRoutes } from './api/_matrix/key/server';
3131
import { getMatrixMediaRoutes } from './api/_matrix/media';
3232
import { getMatrixProfilesRoutes } from './api/_matrix/profiles';
@@ -88,6 +88,24 @@ export const extractDomainFromMatrixUserId = (mxid: string): string => {
8888
return mxid.substring(separatorIndex + 1);
8989
};
9090

91+
/**
92+
* Extract the username and the servername from a matrix user id
93+
* if the serverName is the same as the serverName in the mxid, return only the username (rocket.chat regular username)
94+
* otherwise, return the full mxid and the servername
95+
*/
96+
export const getUsernameServername = (mxid: string, serverName: string): [mxid: string, serverName: string, isLocal: boolean] => {
97+
const senderServerName = extractDomainFromMatrixUserId(mxid);
98+
// if the serverName is the same as the serverName in the mxid, return only the username (rocket.chat regular username)
99+
if (serverName === senderServerName) {
100+
const separatorIndex = mxid.indexOf(':', 1);
101+
if (separatorIndex === -1) {
102+
throw new Error(`Invalid federated username: ${mxid}`);
103+
}
104+
return [mxid.substring(1, separatorIndex), senderServerName, true]; // removers also the @
105+
}
106+
107+
return [mxid, senderServerName, false];
108+
};
91109
/**
92110
* Helper function to create a federated user
93111
*
@@ -99,7 +117,7 @@ export async function createOrUpdateFederatedUser(options: {
99117
name?: string;
100118
origin: string;
101119
}): Promise<string> {
102-
const { username, name = username } = options;
120+
const { username, name = username, origin } = options;
103121

104122
const result = await Users.updateOne(
105123
{
@@ -108,7 +126,7 @@ export async function createOrUpdateFederatedUser(options: {
108126
{
109127
$set: {
110128
username,
111-
name,
129+
name: name || username,
112130
type: 'user' as const,
113131
status: UserStatus.OFFLINE,
114132
active: true,
@@ -294,7 +312,12 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
294312

295313
async created(): Promise<void> {
296314
try {
297-
registerEvents(this.eventHandler, this.serverName, { typing: this.processEDUTyping, presence: this.processEDUPresence });
315+
registerEvents(
316+
this.eventHandler,
317+
this.serverName,
318+
{ typing: this.processEDUTyping, presence: this.processEDUPresence },
319+
this.homeserverServices,
320+
);
298321
} catch (error) {
299322
this.logger.warn('Homeserver module not available, running in limited mode');
300323
}
@@ -669,24 +692,23 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
669692
}
670693
}
671694

672-
async inviteUsersToRoom(room: IRoomNativeFederated, usersUserName: string[], inviter: IUser): Promise<void> {
695+
async inviteUsersToRoom(room: IRoomNativeFederated, matrixUsersUsername: string[], inviter: IUser): Promise<void> {
673696
try {
674697
const inviterUserId = `@${inviter.username}:${this.serverName}`;
675698

676699
await Promise.all(
677-
usersUserName
678-
.filter((username) => {
679-
const isExternalUser = username.includes(':');
680-
return isExternalUser;
681-
})
682-
.map(async (username) => {
683-
const alreadyMember = await Subscriptions.findOneByRoomIdAndUsername(room._id, username, { projection: { _id: 1 } });
684-
if (alreadyMember) {
685-
return;
686-
}
700+
matrixUsersUsername.map(async (username) => {
701+
if (validateFederatedUsername(username)) {
702+
return this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId);
703+
}
704+
const result = await this.homeserverServices.invite.inviteUserToRoom(
705+
`@${username}:${this.serverName}`,
706+
room.federation.mrid,
707+
inviterUserId,
708+
);
687709

688-
await this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId);
689-
}),
710+
return acceptInvite(result.event, username, this.homeserverServices);
711+
}),
690712
);
691713
} catch (error) {
692714
this.logger.error({ msg: 'Failed to invite an user to Matrix:', err: error });

ee/packages/federation-matrix/src/api/_matrix/invite.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Room } from '@rocket.chat/core-services';
2-
import type { IUser } from '@rocket.chat/core-typings';
2+
import { isUserNativeFederated, type IUser } from '@rocket.chat/core-typings';
33
import type {
44
HomeserverServices,
55
RoomService,
@@ -12,7 +12,7 @@ import { Router } from '@rocket.chat/http-router';
1212
import { Users, Rooms } from '@rocket.chat/models';
1313
import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv';
1414

15-
import { createOrUpdateFederatedUser } from '../../FederationMatrix';
15+
import { createOrUpdateFederatedUser, getUsernameServername } from '../../FederationMatrix';
1616

1717
const EventBaseSchema = {
1818
type: 'object',
@@ -279,15 +279,52 @@ export async function joinRoom({
279279
}
280280

281281
await Room.addUserToRoom(internalRoomId, { _id: user._id }, { _id: senderUserId, username: inviteEvent.sender });
282-
283-
// TODO is this needed?
284-
// if (isDM) {
285-
// await MatrixBridgedRoom.createOrUpdateByLocalRoomId(internalRoomId, inviteEvent.roomId, matrixRoom.origin);
286-
// }
287282
}
288283

289284
export const startJoiningRoom = runWithBackoff(joinRoom);
290285

286+
// This is a special case where inside rocket chat we invite users inside rockechat, so if the sender or the invitee are external iw should throw an error
287+
export const acceptInvite = async (
288+
inviteEvent: PersistentEventBase<RoomVersion, 'm.room.member'>,
289+
username: string,
290+
services: HomeserverServices,
291+
) => {
292+
if (!inviteEvent.stateKey) {
293+
throw new Error('join event has missing state key, unable to determine user to join');
294+
}
295+
296+
const internalMappedRoom = await Rooms.findOne({ 'federation.mrid': inviteEvent.roomId });
297+
if (!internalMappedRoom) {
298+
throw new Error('room not found not processing invite');
299+
}
300+
301+
const inviter = await Users.findOneByUsername<Pick<IUser, '_id' | 'username'>>(
302+
getUsernameServername(inviteEvent.sender, services.config.serverName)[0],
303+
{
304+
projection: { _id: 1, username: 1 },
305+
},
306+
);
307+
308+
if (!inviter) {
309+
throw new Error('Sender user ID not found');
310+
}
311+
if (isUserNativeFederated(inviter)) {
312+
throw new Error('Sender user is native federated');
313+
}
314+
315+
const user = await Users.findOneByUsername<Pick<IUser, '_id' | 'username'>>(username, { projection: { _id: 1 } });
316+
317+
// we cannot accept invites from users that are external
318+
if (!user) {
319+
throw new Error('User not found');
320+
}
321+
if (isUserNativeFederated(user)) {
322+
throw new Error('User is native federated');
323+
}
324+
325+
await services.room.joinUser(inviteEvent.roomId, inviteEvent.stateKey);
326+
};
327+
291328
export const getMatrixInviteRoutes = (services: HomeserverServices) => {
292329
const { invite, state, room } = services;
293330

ee/packages/federation-matrix/src/events/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Emitter } from '@rocket.chat/emitter';
2-
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
2+
import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/federation-sdk';
33

44
import { edus } from './edu';
55
import { member } from './member';
@@ -12,11 +12,12 @@ export function registerEvents(
1212
emitter: Emitter<HomeserverEventSignatures>,
1313
serverName: string,
1414
eduProcessTypes: { typing: boolean; presence: boolean },
15+
services: HomeserverServices,
1516
) {
1617
ping(emitter);
1718
message(emitter, serverName);
1819
reaction(emitter);
19-
member(emitter);
20+
member(emitter, services);
2021
edus(emitter, eduProcessTypes);
2122
room(emitter);
2223
}

ee/packages/federation-matrix/src/events/member.ts

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Room } from '@rocket.chat/core-services';
2-
import { UserStatus } from '@rocket.chat/core-typings';
32
import type { Emitter } from '@rocket.chat/emitter';
4-
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
3+
import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/federation-sdk';
54
import { Logger } from '@rocket.chat/logger';
6-
import { Rooms, Users } from '@rocket.chat/models';
5+
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
6+
7+
import { createOrUpdateFederatedUser, getUsernameServername } from '../FederationMatrix';
78

89
const logger = new Logger('federation-matrix:member');
910

@@ -39,60 +40,55 @@ async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver
3940
}
4041
}
4142

42-
async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) {
43+
async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership'], services: HomeserverServices) {
4344
const room = await Rooms.findOne({ 'federation.mrid': data.room_id });
4445
if (!room) {
4546
logger.warn(`No bridged room found for room_id: ${data.room_id}`);
4647
return;
4748
}
4849

49-
const internalUsername = data.sender;
50-
const localUser = await Users.findOneByUsername(internalUsername);
50+
const [username, serverName, isLocal] = getUsernameServername(data.sender, services.config.serverName);
51+
52+
// for local users we must to remove the @ and the server domain
53+
const localUser = isLocal && (await Users.findOneByUsername(username));
54+
5155
if (localUser) {
56+
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, localUser._id);
57+
if (subscription) {
58+
return;
59+
}
5260
await Room.addUserToRoom(room._id, localUser);
5361
return;
5462
}
5563

56-
const [, serverName] = data.sender.split(':');
5764
if (!serverName) {
5865
throw new Error('Invalid sender format, missing server name');
5966
}
6067

61-
const { insertedId } = await Users.insertOne({
62-
username: internalUsername,
63-
type: 'user',
64-
status: UserStatus.OFFLINE,
65-
active: true,
66-
roles: ['user'],
67-
name: data.content.displayname || internalUsername,
68-
requirePasswordChange: false,
69-
createdAt: new Date(),
70-
_updatedAt: new Date(),
71-
federated: true,
72-
federation: {
73-
version: 1,
74-
mui: data.sender,
75-
origin: serverName,
76-
},
68+
const insertedId = await createOrUpdateFederatedUser({
69+
username: data.state_key as `@${string}:${string}`,
70+
origin: serverName,
71+
name: data.content.displayname || (data.state_key as `@${string}:${string}`),
7772
});
7873

7974
const user = await Users.findOneById(insertedId);
75+
8076
if (!user) {
8177
console.warn(`User with ID ${insertedId} not found after insertion`);
8278
return;
8379
}
8480
await Room.addUserToRoom(room._id, user);
8581
}
8682

87-
export function member(emitter: Emitter<HomeserverEventSignatures>) {
83+
export function member(emitter: Emitter<HomeserverEventSignatures>, services: HomeserverServices) {
8884
emitter.on('homeserver.matrix.membership', async (data) => {
8985
try {
9086
if (data.content.membership === 'leave') {
9187
return membershipLeaveAction(data);
9288
}
9389

9490
if (data.content.membership === 'join') {
95-
return membershipJoinAction(data);
91+
return membershipJoinAction(data, services);
9692
}
9793

9894
logger.debug(`Ignoring membership event with membership: ${data.content.membership}`);

0 commit comments

Comments
 (0)