Skip to content

Commit

Permalink
[NEW] Feat/federation channel (#25926)
Browse files Browse the repository at this point in the history
* feat: invite users on channel creation server (WIP)

* feat: backend of federation channels

* chore: add more test cases

* fix: lgtm warning

* fix: remove listeners when license is invalid

* feat: add validation when add users

* fix: fix tests

* fix: fix on add users (WIP)

* fix: more tweaks (WIP)

* fix: wrong condition

* fix: fix invites

* fix: trying stuff until it works

* fix: resolve promise

* fix: more tweaks

* fix: fixes to support latest develop changes

* [NEW] Add federated users on channel creation (#25986)

* convert useHasLicense to TS

* Make federation setting public

* Allow creation of federated channels

* Create new autocomplete

* Fix state

* Fix broken types

* Icons

* Change Federated hint

* fix: fix errors due to the models migration

* fix: fix lint

Co-authored-by: Marcos Defendi <marcos.defendi@rocket.chat>

* fix: invite local users as well

* fix: try to fix DMs

* fix: trying again (WIP)

* fix: removing logs and fixing lint

* fix: useless condition

* fix: fix broken tests

* fix: fix lint

Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>
  • Loading branch information
MarcosSpessatto and gabriellsh committed Jun 27, 2022
1 parent c01cd10 commit 248309d
Show file tree
Hide file tree
Showing 61 changed files with 1,691 additions and 453 deletions.
7 changes: 6 additions & 1 deletion apps/meteor/app/federation-v2/client/Federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { ValueOf } from '@rocket.chat/core-typings';

import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';

const allowedActionsInFederatedRooms: ValueOf<typeof RoomMemberActions>[] = [RoomMemberActions.REMOVE_USER];
const allowedActionsInFederatedRooms: ValueOf<typeof RoomMemberActions>[] = [
RoomMemberActions.REMOVE_USER,
RoomMemberActions.INVITE,
RoomMemberActions.JOIN,
RoomMemberActions.LEAVE,
];

export class Federation {
public static federationActionAllowed(action: ValueOf<typeof RoomMemberActions>): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ export class FederationRoomServiceReceiver {
if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.LOCAL) {
throw new Error(`Could not find room with external room id: ${externalRoomId}`);
}
const isInviterFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver(
const isInviterFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver(
externalInviterId,
this.rocketSettingsAdapter.getHomeServerDomain(),
);
const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver(
const isInviteeFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver(
externalInviteeId,
this.rocketSettingsAdapter.getHomeServerDomain(),
);
Expand Down Expand Up @@ -116,7 +116,6 @@ export class FederationRoomServiceReceiver {

const federatedInviteeUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId);
const federatedInviterUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId);

if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.REMOTE) {
const members = [federatedInviterUser, federatedInviteeUser] as FederatedUser[];
const newFederatedRoom = FederatedRoom.createInstance(
Expand All @@ -140,6 +139,27 @@ export class FederationRoomServiceReceiver {
federatedInviterUser as FederatedUser,
);
}
if (affectedFederatedRoom?.isDirectMessage() && eventOrigin === EVENT_ORIGIN.REMOTE) {
const membersUsernames = [
...(affectedFederatedRoom.internalReference?.usernames || []),
federatedInviteeUser?.internalReference.username as string,
];
const newFederatedRoom = FederatedRoom.createInstance(
externalRoomId,
normalizedRoomId,
federatedInviterUser as FederatedUser,
RoomType.DIRECT_MESSAGE,
externalRoomName,
);
if (affectedFederatedRoom.internalReference?.usernames?.includes(federatedInviteeUser?.internalReference.username || '')) {
return;
}
await this.rocketRoomAdapter.removeDirectMessageRoom(affectedFederatedRoom);
await this.rocketRoomAdapter.createFederatedRoomForDirectMessage(newFederatedRoom, membersUsernames);
await this.bridge.inviteToRoom(externalRoomId, externalInviterId, externalInviteeId);
return;
}

await this.rocketRoomAdapter.addUserToRoom(
federatedRoom as FederatedRoom,
federatedInviteeUser as FederatedUser,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { IMessage, IUser } from '@rocket.chat/core-typings';
import { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';

import { FederatedRoom } from '../domain/FederatedRoom';
import { FederatedUser } from '../domain/FederatedUser';
Expand Down Expand Up @@ -52,14 +52,14 @@ export class FederationRoomServiceSender {
}
const federatedInviterUser = (await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId)) as FederatedUser;
const federatedInviteeUser = (await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId)) as FederatedUser;
const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver(
const isInviteeFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver(
rawInviteeId,
this.rocketSettingsAdapter.getHomeServerDomain(),
);
const internalRoomId = FederatedRoom.buildRoomIdForDirectMessages(federatedInviterUser, federatedInviteeUser);

if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) {
const externalRoomId = await this.bridge.createDirectMessageRoom(federatedInviterUser.externalId, federatedInviteeUser.externalId);
const externalRoomId = await this.bridge.createDirectMessageRoom(federatedInviterUser.externalId, [federatedInviteeUser.externalId]);
const newFederatedRoom = FederatedRoom.createInstance(
externalRoomId,
externalRoomId,
Expand All @@ -68,14 +68,14 @@ export class FederationRoomServiceSender {
'',
[federatedInviterUser, federatedInviteeUser] as any[],
);
await this.rocketRoomAdapter.createFederatedRoomForDirectMessage(newFederatedRoom);
await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom);
}

const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom;
if (isInviteeFromTheSameHomeServer) {
await this.bridge.createUser(
inviteeUsernameOnly,
federatedInviteeUser.internalReference.name as string,
federatedInviteeUser?.internalReference?.name || normalizedInviteeId,
this.rocketSettingsAdapter.getHomeServerDomain(),
);
await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId);
Expand Down Expand Up @@ -112,7 +112,6 @@ export class FederationRoomServiceSender {
if (!federatedRoom) {
throw new Error(`Could not find room id for ${internalRoomId}`);
}

await this.bridge.sendMessage(federatedRoom.externalId, federatedSender.externalId, message.msg);

return message;
Expand All @@ -126,4 +125,26 @@ export class FederationRoomServiceSender {

return Boolean(federatedRoom?.isFederated());
}

public canAddThisUserToTheRoom(internalUser: IUser | string, internalRoom: IRoom): void {
const newUserBeingAdded = typeof internalUser === 'string';
if (newUserBeingAdded) {
return;
}

if ((internalUser as IUser).federated && !internalRoom.federated) {
throw new Error('error-cant-add-federated-users');
}
}

public canAddUsersToTheRoom(internalUser: IUser | string, internalRoom: IRoom): void {
const newUserBeingAdded = typeof internalUser === 'string';
if (newUserBeingAdded) {
return;
}

if ((internalUser as IUser).federated && internalRoom.federated && internalRoom.t !== RoomType.DIRECT_MESSAGE) {
throw new Error('error-this-is-an-ee-feature');
}
}
}
4 changes: 4 additions & 0 deletions apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class FederatedRoom {
return this.internalReference?.federated === true;
}

public isDirectMessage(): boolean {
return this.internalReference?.t === RoomType.DIRECT_MESSAGE;
}

public static buildRoomIdForDirectMessages(inviter: FederatedUser, invitee: FederatedUser): string {
return inviter.internalReference?._id + invitee.internalReference?._id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface IFederationBridge {
onFederationAvailabilityChanged(enabled: boolean): Promise<void>;
getUserProfileInformation(externalUserId: string): Promise<any>;
joinRoom(externalRoomId: string, externalUserId: string): Promise<void>;
createDirectMessageRoom(externalCreatorId: string, externalInviteeId: string): Promise<string>;
createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: string[]): Promise<string>;
inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise<void>;
sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise<void>;
createUser(username: string, name: string, domain: string): Promise<string>;
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/federation-v2/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FederationFactoryEE } from '../../../ee/app/federation-v2/server/infrastructure/Factory';
import { FederationFactory } from './infrastructure/Factory';
import './infrastructure/rocket-chat/slash-commands';

export const FEDERATION_PROCESSING_CONCURRENCY = 1;

Expand All @@ -20,7 +19,7 @@ const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver
federation,
);

const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver);
const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver, rocketSettingsAdapter);

export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender(
rocketRoomAdapter,
Expand All @@ -35,11 +34,13 @@ export const runFederation = async (): Promise<void> => {
await federation.start();

await rocketSettingsAdapter.onFederationEnabledStatusChanged(federation.onFederationAvailabilityChanged.bind(federation));
require('./infrastructure/rocket-chat/slash-commands');

FederationFactory.setupListeners(federationRoomServiceSender);
};

export const stopFederation = async (): Promise<void> => {
FederationFactory.removeListeners();
await federation.stop();
};

Expand Down
20 changes: 16 additions & 4 deletions apps/meteor/app/federation-v2/server/infrastructure/Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,20 @@ export class FederationFactory {
);
}

public static buildEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): MatrixEventsHandler {
return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive));
public static buildEventHandlers(
roomServiceReceive: FederationRoomServiceReceiver,
rocketSettingsAdapter: RocketChatSettingsAdapter,
): MatrixEventsHandler {
return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive, rocketSettingsAdapter));
}

public static getEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): any[] {
public static getEventHandlers(
roomServiceReceive: FederationRoomServiceReceiver,
rocketSettingsAdapter: RocketChatSettingsAdapter,
): any[] {
return [
new MatrixRoomCreatedHandler(roomServiceReceive),
new MatrixRoomMembershipChangedHandler(roomServiceReceive),
new MatrixRoomMembershipChangedHandler(roomServiceReceive, rocketSettingsAdapter),
new MatrixRoomMessageSentHandler(roomServiceReceive),
];
}
Expand All @@ -82,5 +88,11 @@ export class FederationFactory {
FederationHooks.afterLeaveRoom(async (user: IUser, room: IRoom) =>
roomServiceSender.leaveRoom(FederationRoomSenderConverter.toAfterLeaveRoom(user._id, room._id)),
);
FederationHooks.canAddTheUserToTheRoom((user: IUser | string, room: IRoom) => roomServiceSender.canAddThisUserToTheRoom(user, room));
FederationHooks.canAddUsersToTheRoom((user: IUser | string, room: IRoom) => roomServiceSender.canAddUsersToTheRoom(user, room));
}

public static removeListeners(): void {
FederationHooks.removeCEValidation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,21 @@ export class MatrixBridge implements IFederationBridge {
} catch (e) {
bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e);
bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again');

// await this.settingsAdapter.disableFederation();
}
}

public async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
this.isRunning = false;
// the http server might take some minutes to shutdown, and this promise can take some time to be resolved
await this.bridgeInstance?.close();
this.isRunning = false;
}

public async getUserProfileInformation(externalUserId: string): Promise<any> {
try {
return this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId);
return await this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId);
} catch (err) {
// no-op
}
Expand All @@ -71,7 +69,11 @@ export class MatrixBridge implements IFederationBridge {
}

public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise<void> {
await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId);
try {
await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId);
} catch (e) {
// no-op
}
}

public async createUser(username: string, name: string, domain: string): Promise<string> {
Expand All @@ -84,28 +86,26 @@ export class MatrixBridge implements IFederationBridge {
return matrixUserId;
}

public async createDirectMessageRoom(externalCreatorId: string, externalInviteeId: string): Promise<string> {
public async createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: string[]): Promise<string> {
const intent = this.bridgeInstance.getIntent(externalCreatorId);

const visibility = RoomJoinRules.INVITE;
const preset = MatrixRoomType.PRIVATE;

const matrixRoom = await intent.createRoom({
createAsClient: true,
options: {
visibility,
preset,
// eslint-disable-next-line @typescript-eslint/camelcase
is_direct: true,
invite: [externalInviteeId],
invite: externalInviteeIds,
// eslint-disable-next-line @typescript-eslint/camelcase
creation_content: {
// eslint-disable-next-line @typescript-eslint/camelcase
was_internally_programatically_created: true,
},
},
});

return matrixRoom.room_id;
}

Expand Down Expand Up @@ -148,9 +148,6 @@ export class MatrixBridge implements IFederationBridge {
registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput),
disableStores: true,
controller: {
onAliasQuery: (alias, matrixRoomId): void => {
console.log('onAliasQuery', alias, matrixRoomId);
},
onEvent: async (request /* , context*/): Promise<void> => {
// Get the event
const event = request.getData() as unknown as IMatrixEvent<MatrixEventType>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class MatrixRoomReceiverConverter {

public static toChangeRoomMembershipDto(
externalEvent: IMatrixEvent<MatrixEventType.ROOM_MEMBERSHIP_CHANGED>,
homeServerDomain: string,
): FederationRoomChangeMembershipDto {
return Object.assign(new FederationRoomChangeMembershipDto(), {
...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id),
Expand All @@ -39,7 +40,7 @@ export class MatrixRoomReceiverConverter {
normalizedInviteeId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.state_key),
inviteeUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.state_key),
inviterUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.sender),
eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, externalEvent.state_key),
eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, homeServerDomain),
leave: externalEvent.content?.membership === AddMemberToRoomMembership.LEAVE,
});
}
Expand Down Expand Up @@ -68,10 +69,8 @@ export class MatrixRoomReceiverConverter {
return matrixUserId.split(':')[0]?.replace('@', '');
}

protected static getEventOrigin(inviterId = '', inviteeId = ''): EVENT_ORIGIN {
const fromADifferentServer =
MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !==
MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviteeId);
protected static getEventOrigin(inviterId = '', homeServerDomain: string): EVENT_ORIGIN {
const fromADifferentServer = MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== homeServerDomain;

return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL;
}
Expand All @@ -90,13 +89,15 @@ export class MatrixRoomReceiverConverter {
}

protected static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType {
if (matrixRoomIsDirect) {
return RoomType.DIRECT_MESSAGE;
}
const mapping: Record<string, RoomType> = {
[RoomJoinRules.JOIN]: RoomType.CHANNEL,
[RoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP,
};
const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL;

return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType;
return mapping[matrixJoinRule] || RoomType.CHANNEL;
}

protected static tryToGetExternalInfoFromTheRoomState(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver';
import { RocketChatSettingsAdapter } from '../../rocket-chat/adapters/Settings';
import { MatrixRoomReceiverConverter } from '../converters/RoomReceiver';
import { IMatrixEvent } from '../definitions/IMatrixEvent';
import { MatrixEventType } from '../definitions/MatrixEventType';
Expand All @@ -15,12 +16,14 @@ export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler<MatrixEvent
}

export class MatrixRoomMembershipChangedHandler extends MatrixBaseEventHandler<MatrixEventType.ROOM_MEMBERSHIP_CHANGED> {
constructor(private roomService: FederationRoomServiceReceiver) {
constructor(private roomService: FederationRoomServiceReceiver, private rocketSettingsAdapter: RocketChatSettingsAdapter) {
super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED);
}

public async handle(externalEvent: IMatrixEvent<MatrixEventType.ROOM_MEMBERSHIP_CHANGED>): Promise<void> {
await this.roomService.changeRoomMembership(MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent));
await this.roomService.changeRoomMembership(
MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent, this.rocketSettingsAdapter.getHomeServerDomain()),
);
}
}

Expand Down
Loading

0 comments on commit 248309d

Please sign in to comment.