Skip to content
Merged
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
4 changes: 4 additions & 0 deletions apps/meteor/app/federation-v2/server/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class Bridge {
this.isRunning = false;
}

public async getRoomStateByRoomId(userId: string, roomId: string): Promise<Record<string, any>[]> {
return Array.from(((await this.getInstance().getIntent(userId).roomState(roomId)) as IMatrixEvent<MatrixEventType>[]) || []);
}

public getInstance(): MatrixBridge {
return this.bridgeInstance;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export enum AddMemberToRoomMembership {
export interface IMatrixEventContentAddMemberToRoom {
displayname: string;
membership: AddMemberToRoomMembership;
is_direct?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface IMatrixEventContentCreateRoom {
creator: string;
room_version: string;
was_programatically_created?: boolean;
}
195 changes: 169 additions & 26 deletions apps/meteor/app/federation-v2/server/events/createRoom.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,187 @@
import { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { ICreatedRoom } from '@rocket.chat/core-typings';
import { IUser } from '@rocket.chat/apps-engine/definition/users';

import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server';
import { Rooms } from '../../../models/server/raw';
import { createRoom } from '../../../lib/server';
import { IMatrixEvent } from '../definitions/IMatrixEvent';
import { MatrixEventType } from '../definitions/MatrixEventType';
import { checkBridgedRoomExists } from '../methods/checkBridgedRoomExists';
import { matrixClient } from '../matrix-client';
import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules';
import { matrixBridge } from '../bridge';
import { setRoomJoinRules } from './setRoomJoinRules';
import { setRoomName } from './setRoomName';
import { handleRoomMembership } from './roomMembership';

export const handleCreateRoom = async (event: IMatrixEvent<MatrixEventType.CREATE_ROOM>): Promise<void> => {
const { room_id: matrixRoomId, sender } = event;
const removeUselessCharacterFromMatrixRoomId = (matrixRoomId: string): string => {
const prefixedRoomIdOnly = matrixRoomId.split(':')[0];
const prefix = '!';

return new Promise((resolve) => {
setTimeout(async () => {
// Check if the room already exists and if so, ignore
const roomExists = await checkBridgedRoomExists(matrixRoomId);
return prefixedRoomIdOnly?.replace(prefix, '');
};

if (roomExists) {
return resolve();
}
const generateRoomNameForLocalServer = (matrixRoomId: string, matrixRoomName?: string): string => {
return matrixRoomName || `Federation-${removeUselessCharacterFromMatrixRoomId(matrixRoomId)}`;
};

const createLocalRoomAsync = async (roomType: RoomType, roomName: string, creator: IUser, members: IUser[] = []): Promise<ICreatedRoom> => {
return new Promise((resolve) => resolve(createRoom(roomType, roomName, creator.username, members as any[]) as ICreatedRoom));
};

const createBridgedRecordRoom = async (roomId: IRoom['id'], matrixRoomId: string): Promise<void> =>
new Promise((resolve) => resolve(MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId })));

const createLocalUserIfNecessary = async (matrixUserId: string): Promise<string> => {
const { uid } = await matrixClient.user.createLocal(matrixUserId);

// Find the bridged user id
const bridgedUserId = await MatrixBridgedUser.getId(sender);
let user;
return uid;
};

const applyRoomStateIfNecessary = async (matrixRoomId: string, roomState?: IMatrixEvent<MatrixEventType>[]): Promise<void> => {
// TODO: this should be better
/* eslint-disable no-await-in-loop */
for (const state of roomState || []) {
switch (state.type) {
case 'm.room.create':
continue;
case 'm.room.join_rules': {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase
await setRoomJoinRules({ room_id: matrixRoomId, ...state });

// Create the user if necessary
if (!bridgedUserId) {
const { uid } = await matrixClient.user.createLocal(sender);
break;
}
case 'm.room.name': {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase
await setRoomName({ room_id: matrixRoomId, ...state });

break;
}
case 'm.room.member': {
// @ts-ignore
if (state.content.membership === 'join') {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define
await handleRoomMembership({ room_id: matrixRoomId, ...state });
}

user = Users.findOneById(uid);
} else {
user = await Users.findOneById(bridgedUserId);
break;
}
}
}
/* eslint-enable no-await-in-loop */
};

const mapLocalAndExternal = async (roomId: string, matrixRoomId: string): Promise<void> => {
await createBridgedRecordRoom(roomId, matrixRoomId);
await Rooms.setAsBridged(roomId);
};

const tryToGetDataFromExternalRoom = async (
senderMatrixUserId: string,
matrixRoomId: string,
roomState: IMatrixEvent<MatrixEventType>[] = [],
): Promise<Record<string, any>> => {
const finalRoomState =
roomState && roomState?.length > 0 ? roomState : await matrixBridge.getRoomStateByRoomId(senderMatrixUserId, matrixRoomId);
const externalRoomName = finalRoomState.find((stateEvent: Record<string, any>) => stateEvent.type === MatrixEventType.SET_ROOM_NAME)
?.content?.name;
const externalRoomJoinRule = finalRoomState.find(
(stateEvent: Record<string, any>) => stateEvent.type === MatrixEventType.SET_ROOM_JOIN_RULES,
)?.content?.join_rule;

// Create temp room name
const roomName = `Federation-${matrixRoomId.split(':')[0].replace('!', '')}`;
return {
externalRoomName,
externalRoomJoinRule,
};
};

export const createLocalDirectMessageRoom = async (matrixRoomId: string, creator: IUser, affectedUser: IUser): Promise<IRoom['id']> => {
const { _id: roomId } = await createLocalRoomAsync(RoomType.DIRECT_MESSAGE, generateRoomNameForLocalServer(matrixRoomId), creator, [
creator,
affectedUser,
]);
await mapLocalAndExternal(roomId, matrixRoomId);

return roomId;
};

export const getLocalRoomType = (matrixJoinRule = '', matrixRoomIsDirect = false): RoomType => {
const mapping: Record<string, RoomType> = {
[SetRoomJoinRules.JOIN]: RoomType.CHANNEL,
[SetRoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP,
};
const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL;

return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType;
};

export const createLocalChannelsRoom = async (
matrixRoomId: string,
senderMatrixUserId: string,
creator: IUser,
roomState?: IMatrixEvent<MatrixEventType>[],
): Promise<IRoom['id']> => {
let roomName = '';
let joinRule;

try {
const { externalRoomName, externalRoomJoinRule } = await tryToGetDataFromExternalRoom(senderMatrixUserId, matrixRoomId, roomState);
roomName = externalRoomName;
joinRule = externalRoomJoinRule;
} catch (err) {
// no-op
}
const { rid: roomId } = await createLocalRoomAsync(
getLocalRoomType(joinRule),
generateRoomNameForLocalServer(matrixRoomId, roomName),
creator,
);
await mapLocalAndExternal(roomId, matrixRoomId);

return roomId;
};

export const processFirstAccessFromExternalServer = async (
matrixRoomId: string,
senderMatrixUserId: string,
affectedMatrixUserId: string,
senderUser: IUser,
affectedUser: IUser,
isDirect = false,
roomState: IMatrixEvent<MatrixEventType>[],
): Promise<string> => {
let roomId;
if (isDirect) {
roomId = await createLocalDirectMessageRoom(matrixRoomId, senderUser, affectedUser);
} else {
roomId = await createLocalChannelsRoom(matrixRoomId, senderMatrixUserId, senderUser, roomState);
}

await applyRoomStateIfNecessary(matrixRoomId, roomState);
await matrixBridge.getInstance().getIntent(affectedMatrixUserId).join(matrixRoomId);

return roomId;
};

export const handleCreateRoom = async (event: IMatrixEvent<MatrixEventType.CREATE_ROOM>): Promise<void> => {
const {
room_id: matrixRoomId,
sender,
content: { was_programatically_created: wasProgramaticallyCreated = false },
} = event;

// @ts-ignore TODO: typing of legacy functions
const { rid: roomId } = createRoom('c', roomName, user.username);
// Check if the room already exists and if so, ignore
const roomExists = await checkBridgedRoomExists(matrixRoomId);
if (roomExists || wasProgramaticallyCreated) {
return;
}

MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId });
const bridgedUserId = await MatrixBridgedUser.getId(sender);
const creator = await Users.findOneById(bridgedUserId || (await createLocalUserIfNecessary(sender)));

resolve();
}, 500);
});
await createLocalChannelsRoom(matrixRoomId, sender, creator);
};
110 changes: 31 additions & 79 deletions apps/meteor/app/federation-v2/server/events/roomMembership.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,35 @@
import { MatrixBridgedUser, MatrixBridgedRoom, Users, Rooms } from '../../../models/server';
import { addUserToRoom, createRoom, removeUserFromRoom } from '../../../lib/server';
import { IUser } from '@rocket.chat/apps-engine/definition/users';

import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server';
import { addUserToRoom, removeUserFromRoom } from '../../../lib/server';
import { IMatrixEvent } from '../definitions/IMatrixEvent';
import { MatrixEventType } from '../definitions/MatrixEventType';
import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom';
import { setRoomJoinRules } from './setRoomJoinRules';
import { setRoomName } from './setRoomName';
import { addToQueue } from '../queue';
import { matrixClient } from '../matrix-client';
import { processFirstAccessFromExternalServer } from './createRoom';

const ensureRoom = async (
matrixRoomId: string,
roomId: string,
username: string,
roomState?: IMatrixEvent<MatrixEventType>[],
): Promise<string> => {
const room = await Rooms.findOneById(roomId);
// If the room does not exist, create it
if (!room) {
// Create temp room name
const roomName = `Federation-${matrixRoomId.split(':')[0].replace('!', '')}`;

// @ts-ignore TODO: typing of legacy functions
const { rid: createdRoomId } = createRoom('c', roomName, username);

roomId = createdRoomId;

MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId });

// TODO: this should be better
/* eslint-disable no-await-in-loop */
for (const state of roomState || []) {
switch (state.type) {
case 'm.room.create':
continue;
case 'm.room.join_rules': {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase
await setRoomJoinRules({ room_id: roomId, ...state });
const extractServerNameFromMatrixUserId = (matrixRoomId = ''): string => matrixRoomId.split(':')[1];

break;
}
case 'm.room.name': {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase
await setRoomName({ room_id: roomId, ...state });

break;
}
case 'm.room.member': {
// @ts-ignore
if (state.content.membership === 'join') {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define
await handleRoomMembership({ room_id: roomId, ...state });
}

break;
}
}
}
/* eslint-enable no-await-in-loop */
}

return roomId;
const addUserToRoomAsync = async (roomId: string, affectedUser: IUser, senderUser?: IUser): Promise<void> => {
new Promise((resolve) => resolve(addUserToRoom(roomId, affectedUser as any, senderUser as any)));
};

export const handleRoomMembership = async (event: IMatrixEvent<MatrixEventType.ROOM_MEMBERSHIP>): Promise<void> => {
const {
room_id: matrixRoomId,
sender: senderMatrixUserId,
state_key: affectedMatrixUserId,
content: { membership },
content: { membership, is_direct: isDirect = false },
invite_room_state: roomState,
} = event;

// Find the bridged room id
let roomId = await MatrixBridgedRoom.getId(matrixRoomId);
const fromADifferentServer =
extractServerNameFromMatrixUserId(senderMatrixUserId) !== extractServerNameFromMatrixUserId(affectedMatrixUserId);

// If there is no room id, throw error
if (!roomId) {
if (!roomId && !fromADifferentServer) {
throw new Error(`Could not find room with matrixRoomId: ${matrixRoomId}`);
}

Expand All @@ -102,31 +53,32 @@ export const handleRoomMembership = async (event: IMatrixEvent<MatrixEventType.R
affectedUser = Users.findOneById(uid);
}

if (!roomId && fromADifferentServer) {
roomId = await processFirstAccessFromExternalServer(
matrixRoomId,
senderMatrixUserId,
affectedMatrixUserId,
senderUser,
affectedUser,
isDirect,
roomState as IMatrixEvent<MatrixEventType>[],
);
}

if (!roomId) {
return;
}

switch (membership) {
case AddMemberToRoomMembership.JOIN:
roomId = await ensureRoom(matrixRoomId, roomId, senderUser.username, roomState);

addUserToRoom(roomId, affectedUser);
await addUserToRoomAsync(roomId, affectedUser);
break;
case AddMemberToRoomMembership.INVITE:
// Re-run the state first
if (!roomId) {
for (const state of roomState || []) {
// eslint-disable-next-line @typescript-eslint/camelcase,no-await-in-loop
addToQueue({ ...state, room_id: matrixRoomId });
}

addToQueue(event);

return;
}

// If the room exists, then just add the user
// TODO: this should be a local invite
addUserToRoom(roomId, affectedUser, senderUser);
await addUserToRoomAsync(roomId, affectedUser, senderUser);
break;
case AddMemberToRoomMembership.LEAVE:
removeUserFromRoom(roomId, affectedUser, {
await removeUserFromRoom(roomId, affectedUser, {
byUser: senderUser,
});
break;
Expand Down
Loading