diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts new file mode 100644 index 000000000000..49898c6ec2a7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts @@ -0,0 +1,217 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { EVENT_ORIGIN, IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from './input/RoomReceiverDto'; + +export class FederationRoomServiceReceiver { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketMessageAdapter: RocketChatMessageAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async createRoom(roomCreateInput: FederationRoomCreateInputDto): Promise { + const { + externalRoomId, + externalInviterId, + normalizedInviterId, + externalRoomName, + normalizedRoomId, + roomType, + wasInternallyProgramaticallyCreated = false, + } = roomCreateInput; + + if ((await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)) || wasInternallyProgramaticallyCreated) { + return; + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation?.displayname || normalizedInviterId; + const federatedCreatorUser = FederatedUser.createInstance(externalInviterId, { + name, + username: normalizedInviterId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedCreatorUser); + } + const creator = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + creator as FederatedUser, + roomType || RoomType.CHANNEL, + externalRoomName, + ); + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + } + + public async changeRoomMembership(roomChangeMembershipInput: FederationRoomChangeMembershipDto): Promise { + const { + externalRoomId, + normalizedInviteeId, + normalizedRoomId, + normalizedInviterId, + externalRoomName, + externalInviteeId, + externalInviterId, + inviteeUsernameOnly, + inviterUsernameOnly, + eventOrigin, + roomType, + leave, + } = roomChangeMembershipInput; + const affectedFederatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.LOCAL) { + throw new Error(`Could not find room with external room id: ${externalRoomId}`); + } + const isInviterFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviterId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + externalInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviterId); + const name = externalUserProfileInformation.displayname || normalizedInviterId; + const username = isInviterFromTheSameHomeServer ? inviterUsernameOnly : normalizedInviterId; + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name, + username, + existsOnlyOnProxyServer: isInviterFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalInviteeId); + const name = externalUserProfileInformation.displayname || normalizedInviteeId; + const username = isInviteeFromTheSameHomeServer ? inviteeUsernameOnly : normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(externalInviteeId, { + name, + username, + existsOnlyOnProxyServer: isInviteeFromTheSameHomeServer, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + 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 any[]; + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + federatedInviterUser as FederatedUser, + roomType, + externalRoomName, + members, + ); + + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + await this.bridge.joinRoom(externalRoomId, externalInviteeId); + } + const federatedRoom = affectedFederatedRoom || (await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId)); + + if (leave) { + return this.rocketRoomAdapter.removeUserFromRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + await this.rocketRoomAdapter.addUserToRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + + public async receiveExternalMessage(roomSendInternalMessageInput: FederationRoomSendInternalMessageDto): Promise { + const { externalRoomId, externalSenderId, text } = roomSendInternalMessageInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const senderUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!senderUser) { + return; + } + + await this.rocketMessageAdapter.sendMessage(senderUser, text, federatedRoom); + } + + public async changeJoinRules(roomJoinRulesChangeInput: FederationRoomChangeJoinRulesDto): Promise { + const { externalRoomId, roomType } = roomJoinRulesChangeInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.setRoomType(roomType); + await this.rocketRoomAdapter.updateRoomType(federatedRoom); + } + + public async changeRoomName(roomChangeNameInput: FederationRoomChangeNameDto): Promise { + const { externalRoomId, normalizedRoomName } = roomChangeNameInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomName(normalizedRoomName); + + await this.rocketRoomAdapter.updateRoomName(federatedRoom); + } + + public async changeRoomTopic(roomChangeTopicInput: FederationRoomChangeTopicDto): Promise { + const { externalRoomId, roomTopic } = roomChangeTopicInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + federatedRoom.changeRoomTopic(roomTopic); + + await this.rocketRoomAdapter.updateRoomTopic(federatedRoom); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts new file mode 100644 index 000000000000..2be2283ddd86 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts @@ -0,0 +1,128 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedRoom } from '../domain/FederatedRoom'; +import { FederatedUser } from '../domain/FederatedUser'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from '../infrastructure/rocket-chat/adapters/Notification'; +import { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from './input/RoomSenderDto'; + +export class FederationRoomServiceSender { + constructor( + private rocketRoomAdapter: RocketChatRoomAdapter, + private rocketUserAdapter: RocketChatUserAdapter, + private rocketSettingsAdapter: RocketChatSettingsAdapter, + private rocketNotificationAdapter: RocketChatNotificationAdapter, + private bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async inviteUserToAFederatedRoom(roomInviteUserInput: FederationRoomInviteUserDto): Promise { + const { normalizedInviteeId, rawInviteeId, internalInviterId, inviteeUsernameOnly, internalRoomId } = roomInviteUserInput; + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId))) { + const internalUser = (await this.rocketUserAdapter.getInternalUserById(internalInviterId)) as IUser; + const externalInviterId = await this.bridge.createUser( + internalUser.username as string, + internalUser.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const federatedInviterUser = FederatedUser.createInstance(externalInviterId, { + name: internalUser.name as string, + username: internalUser.username as string, + existsOnlyOnProxyServer: true, + }); + await this.rocketUserAdapter.createFederatedUser(federatedInviterUser); + } + + if (!(await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId))) { + const externalUserProfileInformation = await this.bridge.getUserProfileInformation(rawInviteeId); + const name = externalUserProfileInformation?.displayname || normalizedInviteeId; + const federatedInviteeUser = FederatedUser.createInstance(rawInviteeId, { + name, + username: normalizedInviteeId, + existsOnlyOnProxyServer: false, + }); + + await this.rocketUserAdapter.createFederatedUser(federatedInviteeUser); + } + + const federatedInviterUser = (await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId)) as FederatedUser; + const federatedInviteeUser = (await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId)) as FederatedUser; + const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + rawInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) { + const internalRoom = (await this.rocketRoomAdapter.getInternalRoomById(internalRoomId)) as IRoom; + const roomName = (internalRoom.fname || internalRoom.name) as string; + const externalRoomId = await this.bridge.createRoom( + federatedInviterUser.externalId, + federatedInviteeUser.externalId, + internalRoom.t as RoomType, + roomName, + internalRoom.topic, + ); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + externalRoomId, + federatedInviterUser, + internalRoom.t as RoomType, + roomName, + ); + await this.rocketRoomAdapter.updateFederatedRoomByInternalRoomId(internalRoom._id, newFederatedRoom); + } + + const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom; + const wasInvitedWhenTheRoomWasCreated = federatedRoom.isDirectMessage(); + if (isInviteeFromTheSameHomeServer) { + await this.bridge.createUser( + inviteeUsernameOnly, + federatedInviteeUser.internalReference.name as string, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId); + await this.bridge.joinRoom(federatedRoom.externalId, federatedInviteeUser.externalId); + } else if (!wasInvitedWhenTheRoomWasCreated) { + this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId).catch(() => { + this.rocketNotificationAdapter.notifyWithEphemeralMessage( + 'Federation_Matrix_only_owners_can_invite_users', + federatedInviterUser?.internalReference?._id, + internalRoomId, + federatedInviterUser?.internalReference?.language, + ); + }); + } + await this.rocketRoomAdapter.addUserToRoom(federatedRoom, federatedInviteeUser, federatedInviterUser); + } + + public async sendMessageFromRocketChat(roomSendExternalMessageInput: FederationRoomSendExternalMessageDto): Promise { + const { internalRoomId, internalSenderId, message } = roomSendExternalMessageInput; + + const federatedSender = await this.rocketUserAdapter.getFederatedUserByInternalId(internalSenderId); + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + if (!federatedSender) { + throw new Error(`Could not find user id for ${internalSenderId}`); + } + if (!federatedRoom) { + throw new Error(`Could not find room id for ${internalRoomId}`); + } + + await this.bridge.sendMessage(federatedRoom.externalId, federatedSender.externalId, message.msg); + + return message; + } + + public async isAFederatedRoom(internalRoomId: string): Promise { + if (!internalRoomId) { + return false; + } + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + + return Boolean(federatedRoom?.isFederated()); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts new file mode 100644 index 000000000000..ed85b194d67d --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts @@ -0,0 +1,63 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { EVENT_ORIGIN } from '../../domain/IFederationBridge'; + +class BaseRoom { + externalRoomId: string; + + normalizedRoomId: string; +} + +export class FederationRoomCreateInputDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + wasInternallyProgramaticallyCreated?: boolean; + + externalRoomName?: string; + + roomType?: RoomType; +} + +export class FederationRoomChangeMembershipDto extends BaseRoom { + externalInviterId: string; + + normalizedInviterId: string; + + inviterUsernameOnly: string; + + externalInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; + + roomType: RoomType; + + eventOrigin: EVENT_ORIGIN; + + leave?: boolean; + + externalRoomName?: string; +} + +export class FederationRoomSendInternalMessageDto extends BaseRoom { + externalSenderId: string; + + normalizedSenderId: string; + + text: string; +} + +export class FederationRoomChangeJoinRulesDto extends BaseRoom { + roomType: RoomType; +} + +export class FederationRoomChangeNameDto extends BaseRoom { + normalizedRoomName: string; +} + +export class FederationRoomChangeTopicDto extends BaseRoom { + roomTopic: string; +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts new file mode 100644 index 000000000000..700216105866 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts @@ -0,0 +1,21 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +export class FederationRoomInviteUserDto { + internalInviterId: string; + + internalRoomId: string; + + rawInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; +} + +export class FederationRoomSendExternalMessageDto { + internalRoomId: string; + + internalSenderId: string; + + message: IMessage; +} diff --git a/apps/meteor/app/federation-v2/server/bridge.ts b/apps/meteor/app/federation-v2/server/bridge.ts deleted file mode 100644 index 895e472de021..000000000000 --- a/apps/meteor/app/federation-v2/server/bridge.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { Bridge as MatrixBridge } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; -import { Settings } from '../../models/server/raw'; -import type { IMatrixEvent } from './definitions/IMatrixEvent'; -import type { MatrixEventType } from './definitions/MatrixEventType'; -import { addToQueue } from './queue'; -import { getRegistrationInfo } from './config'; -import { bridgeLogger } from './logger'; - -class Bridge { - private bridgeInstance: MatrixBridge; - - private isRunning = false; - - public async start(): Promise { - try { - await this.stop(); - await this.createInstance(); - - if (!this.isRunning) { - await this.bridgeInstance.run(this.getBridgePort()); - this.isRunning = true; - } - } catch (e) { - bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); - - bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); - Settings.updateValueById('Federation_Matrix_enabled', false); - } - } - - public async stop(): Promise { - if (!this.isRunning) { - return; - } - // the http server can take some minutes to shutdown and this promise to be resolved - await this.bridgeInstance?.close(); - this.isRunning = false; - } - - public async getRoomStateByRoomId(userId: string, roomId: string): Promise[]> { - return Array.from(((await this.getInstance().getIntent(userId).roomState(roomId)) as IMatrixEvent[]) || []); - } - - public getInstance(): MatrixBridge { - return this.bridgeInstance; - } - - private async createInstance(): Promise { - bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); - - // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails - const { Bridge: MatrixBridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); - - this.bridgeInstance = new MatrixBridge({ - homeserverUrl: settings.get('Federation_Matrix_homeserver_url'), - domain: settings.get('Federation_Matrix_homeserver_domain'), - registration: AppServiceRegistration.fromObject(getRegistrationInfo()), - disableStores: true, - controller: { - onAliasQuery: (alias, matrixRoomId): void => { - console.log('onAliasQuery', alias, matrixRoomId); - }, - onEvent: async (request /* , context*/): Promise => { - // Get the event - const event = request.getData() as unknown as IMatrixEvent; - - addToQueue(event); - }, - onLog: async (line, isError): Promise => { - console.log(line, isError); - }, - }, - }); - } - - private getBridgePort(): number { - const [, , port] = settings.get('Federation_Matrix_bridge_url').split(':'); - - return parseInt(port); - } -} - -export const matrixBridge = new Bridge(); diff --git a/apps/meteor/app/federation-v2/server/config.ts b/apps/meteor/app/federation-v2/server/config.ts deleted file mode 100644 index d1bad2455d80..000000000000 --- a/apps/meteor/app/federation-v2/server/config.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AppServiceOutput } from '@rocket.chat/forked-matrix-appservice-bridge'; - -import { settings } from '../../settings/server'; - -export type bridgeUrlTuple = [string, string, number]; - -export function getRegistrationInfo(): AppServiceOutput { - /* eslint-disable @typescript-eslint/camelcase */ - return { - id: settings.get('Federation_Matrix_id'), - hs_token: settings.get('Federation_Matrix_hs_token'), - as_token: settings.get('Federation_Matrix_as_token'), - url: settings.get('Federation_Matrix_bridge_url'), - sender_localpart: settings.get('Federation_Matrix_bridge_localpart'), - namespaces: { - users: [ - { - exclusive: false, - // Reserve these MXID's (usernames) - regex: `.*`, - }, - ], - aliases: [ - { - exclusive: false, - // Reserve these room aliases - regex: `.*`, - }, - ], - rooms: [ - { - exclusive: false, - // This regex is used to define which rooms we listen to with the bridge. - // This does not reserve the rooms like the other namespaces. - regex: '.*', - }, - ], - }, - rate_limited: false, - protocols: null, - }; - /* eslint-enable @typescript-eslint/camelcase */ -} diff --git a/apps/meteor/app/federation-v2/server/data-interface/index.ts b/apps/meteor/app/federation-v2/server/data-interface/index.ts deleted file mode 100644 index 18e2fbf7020f..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const dataInterface = { - message: message.normalize, - room: room.normalize, - user: user.normalize, -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/message.ts b/apps/meteor/app/federation-v2/server/data-interface/message.ts deleted file mode 100644 index 7d27732f93e6..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/message.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IMessage, IUser } from '@rocket.chat/core-typings'; - -import { dataInterface } from '.'; - -interface INormalizedMessage extends IMessage { - u: Required>; -} - -export const normalize = async (message: IMessage): Promise => { - // TODO: normalize the entire payload (if needed) - const normalizedMessage: INormalizedMessage = message as INormalizedMessage; - - // Normalize the user - normalizedMessage.u = (await dataInterface.user(message.u._id)) as Required>; - - return normalizedMessage; -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/room.ts b/apps/meteor/app/federation-v2/server/data-interface/room.ts deleted file mode 100644 index df1d2163badf..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/room.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; - -import { Rooms } from '../../../models/server'; - -export const normalize = async (roomId: string): Promise => { - // Normalize the user - return Rooms.findOneById(roomId); -}; diff --git a/apps/meteor/app/federation-v2/server/data-interface/user.ts b/apps/meteor/app/federation-v2/server/data-interface/user.ts deleted file mode 100644 index 15fb48843428..000000000000 --- a/apps/meteor/app/federation-v2/server/data-interface/user.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IUser } from '@rocket.chat/core-typings'; - -import { Users } from '../../../models/server'; - -export const normalize = async (userId: string): Promise => { - // Normalize the user - return Users.findOneById(userId); -}; diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts new file mode 100644 index 000000000000..c82b078c49e7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -0,0 +1,78 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IRoom, IUser } from '@rocket.chat/core-typings'; + +import { FederatedUser } from './FederatedUser'; + +export class FederatedRoom { + public externalId: string; + + public members?: FederatedUser[]; + + public internalReference: IRoom; + + // eslint-disable-next-line + private constructor() {} + + private static generateTemporaryName(normalizedExternalId: string): string { + return `Federation-${normalizedExternalId}`; + } + + public static createInstance( + externalId: string, + normalizedExternalId: string, + creator: FederatedUser, + type: RoomType, + name?: string, + members?: IUser[], + ): FederatedRoom { + const roomName = name || FederatedRoom.generateTemporaryName(normalizedExternalId); + return Object.assign(new FederatedRoom(), { + externalId, + ...(type === RoomType.DIRECT_MESSAGE ? { members } : {}), + internalReference: { + t: type, + name: roomName, + fname: roomName, + u: creator.internalReference, + }, + }); + } + + public static build(): FederatedRoom { + return new FederatedRoom(); + } + + public isDirectMessage(): boolean { + return this.internalReference?.t === RoomType.DIRECT_MESSAGE; + } + + public setRoomType(type: RoomType): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message type'); + } + this.internalReference.t = type; + } + + public changeRoomName(name: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message name'); + } + this.internalReference.name = name; + this.internalReference.fname = name; + } + + public changeRoomTopic(topic: string): void { + if (this.isDirectMessage()) { + throw new Error('Its not possible to change a direct message topic'); + } + this.internalReference.description = topic; + } + + public getMembers(): IUser[] { + return this.isDirectMessage() && this.members && this.members.length > 0 ? this.members.map((user) => user.internalReference) : []; + } + + public isFederated(): boolean { + return this.internalReference?.federated === true; + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts new file mode 100644 index 000000000000..225b0fabc003 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedUser.ts @@ -0,0 +1,38 @@ +import { IUser } from '@rocket.chat/core-typings'; + +export interface IFederatedUserCreationParams { + name: string; + username: string; + existsOnlyOnProxyServer: boolean; +} + +export class FederatedUser { + public externalId: string; + + public internalReference: IUser; + + public existsOnlyOnProxyServer: boolean; + + // eslint-disable-next-line + private constructor() {} + + public static createInstance(externalId: string, params: IFederatedUserCreationParams): FederatedUser { + return Object.assign(new FederatedUser(), { + externalId, + existsOnlyOnProxyServer: params.existsOnlyOnProxyServer, + internalReference: { + username: params.username, + name: params.name, + type: 'user', + status: 'online', + active: true, + roles: ['user'], + requirePasswordChange: false, + }, + }); + } + + public static build(): FederatedUser { + return new FederatedUser(); + } +} diff --git a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts new file mode 100644 index 000000000000..86310ec30f17 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -0,0 +1,25 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +export interface IFederationBridge { + start(): Promise; + stop(): Promise; + onFederationAvailabilityChanged(enabled: boolean): Promise; + getUserProfileInformation(externalUserId: string): Promise; + joinRoom(externalRoomId: string, externalUserId: string): Promise; + createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise; + inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise; + sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise; + createUser(username: string, name: string, domain: string): Promise; + isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean; +} + +export enum EVENT_ORIGIN { + LOCAL = 'LOCAL', + REMOTE = 'REMOTE', +} diff --git a/apps/meteor/app/federation-v2/server/eventHandler.ts b/apps/meteor/app/federation-v2/server/eventHandler.ts deleted file mode 100644 index 166ed1199adc..000000000000 --- a/apps/meteor/app/federation-v2/server/eventHandler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { handleRoomMembership, handleCreateRoom, handleSendMessage, setRoomJoinRules, setRoomName, setRoomTopic } from './events'; - -export const eventHandler = async (event: IMatrixEvent): Promise => { - console.log(`Processing ${event.type}...`, JSON.stringify(event, null, 2)); - - switch (event.type) { - case MatrixEventType.CREATE_ROOM: { - await handleCreateRoom(event as IMatrixEvent); - - break; - } - case MatrixEventType.ROOM_MEMBERSHIP: { - await handleRoomMembership(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_JOIN_RULES: { - await setRoomJoinRules(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_NAME: { - await setRoomName(event as IMatrixEvent); - - break; - } - case MatrixEventType.SET_ROOM_TOPIC: { - await setRoomTopic(event as IMatrixEvent); - - break; - } - case MatrixEventType.SEND_MESSAGE: { - await handleSendMessage(event as IMatrixEvent); - - break; - } - // case MatrixEventType.SET_ROOM_POWER_LEVELS: - // case MatrixEventType.SET_ROOM_CANONICAL_ALIAS: - // case MatrixEventType.SET_ROOM_HISTORY_VISIBILITY: - // case MatrixEventType.SET_ROOM_GUEST_ACCESS: { - // console.log(`Ignoring ${event.type}`); - // - // break; - // } - default: - console.log(`Could not find handler for ${event.type}`, event); - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/createRoom.ts b/apps/meteor/app/federation-v2/server/events/createRoom.ts deleted file mode 100644 index 5cca3032c455..000000000000 --- a/apps/meteor/app/federation-v2/server/events/createRoom.ts +++ /dev/null @@ -1,187 +0,0 @@ -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'; - -const removeUselessCharacterFromMatrixRoomId = (matrixRoomId: string): string => { - const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; - const prefix = '!'; - - return prefixedRoomIdOnly?.replace(prefix, ''); -}; - -const generateRoomNameForLocalServer = (matrixRoomId: string, matrixRoomName?: string): string => { - return matrixRoomName || `Federation-${removeUselessCharacterFromMatrixRoomId(matrixRoomId)}`; -}; - -const createLocalRoomAsync = async (roomType: RoomType, roomName: string, creator: IUser, members: IUser[] = []): Promise => { - return new Promise((resolve) => resolve(createRoom(roomType, roomName, creator.username, members as any[]) as ICreatedRoom)); -}; - -const createBridgedRecordRoom = async (roomId: IRoom['id'], matrixRoomId: string): Promise => - new Promise((resolve) => resolve(MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }))); - -const createLocalUserIfNecessary = async (matrixUserId: string): Promise => { - const { uid } = await matrixClient.user.createLocal(matrixUserId); - - return uid; -}; - -const applyRoomStateIfNecessary = async (matrixRoomId: string, roomState?: IMatrixEvent[]): Promise => { - // 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 }); - - 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 }); - } - - break; - } - } - } - /* eslint-enable no-await-in-loop */ -}; - -const mapLocalAndExternal = async (roomId: string, matrixRoomId: string): Promise => { - await createBridgedRecordRoom(roomId, matrixRoomId); - await Rooms.setAsBridged(roomId); -}; - -const tryToGetDataFromExternalRoom = async ( - senderMatrixUserId: string, - matrixRoomId: string, - roomState: IMatrixEvent[] = [], -): Promise> => { - const finalRoomState = - roomState && roomState?.length > 0 ? roomState : await matrixBridge.getRoomStateByRoomId(senderMatrixUserId, matrixRoomId); - const externalRoomName = finalRoomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_NAME) - ?.content?.name; - const externalRoomJoinRule = finalRoomState.find( - (stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_JOIN_RULES, - )?.content?.join_rule; - - return { - externalRoomName, - externalRoomJoinRule, - }; -}; - -export const createLocalDirectMessageRoom = async (matrixRoomId: string, creator: IUser, affectedUser: IUser): Promise => { - 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 = { - [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[], -): Promise => { - 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[], -): Promise => { - 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): Promise => { - const { - room_id: matrixRoomId, - sender, - content: { was_programatically_created: wasProgramaticallyCreated = false }, - } = event; - - // Check if the room already exists and if so, ignore - const roomExists = await checkBridgedRoomExists(matrixRoomId); - if (roomExists || wasProgramaticallyCreated) { - return; - } - - const bridgedUserId = await MatrixBridgedUser.getId(sender); - const creator = await Users.findOneById(bridgedUserId || (await createLocalUserIfNecessary(sender))); - - await createLocalChannelsRoom(matrixRoomId, sender, creator); -}; diff --git a/apps/meteor/app/federation-v2/server/events/index.ts b/apps/meteor/app/federation-v2/server/events/index.ts deleted file mode 100644 index ef403e8e78cd..000000000000 --- a/apps/meteor/app/federation-v2/server/events/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './createRoom'; -export * from './roomMembership'; -export * from './sendMessage'; -export * from './setRoomJoinRules'; -export * from './setRoomName'; -export * from './setRoomTopic'; diff --git a/apps/meteor/app/federation-v2/server/events/roomMembership.ts b/apps/meteor/app/federation-v2/server/events/roomMembership.ts deleted file mode 100644 index d51233c1f14e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/roomMembership.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 { matrixClient } from '../matrix-client'; -import { processFirstAccessFromExternalServer } from './createRoom'; - -const extractServerNameFromMatrixUserId = (matrixRoomId = ''): string => matrixRoomId.split(':')[1]; - -const addUserToRoomAsync = async (roomId: string, affectedUser: IUser, senderUser?: IUser): Promise => { - new Promise((resolve) => resolve(addUserToRoom(roomId, affectedUser as any, senderUser as any))); -}; - -export const handleRoomMembership = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - sender: senderMatrixUserId, - state_key: affectedMatrixUserId, - 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 && !fromADifferentServer) { - throw new Error(`Could not find room with matrixRoomId: ${matrixRoomId}`); - } - - // Find the sender user - const senderUserId = await MatrixBridgedUser.getId(senderMatrixUserId); - let senderUser = await Users.findOneById(senderUserId); - // If the sender user does not exist, it means we need to create it - if (!senderUser) { - const { uid } = await matrixClient.user.createLocal(senderMatrixUserId); - - senderUser = Users.findOneById(uid); - } - - // Find the affected user - const affectedUserId = await MatrixBridgedUser.getId(affectedMatrixUserId); - let affectedUser = await Users.findOneById(affectedUserId); - // If the affected user does not exist, it means we need to create it - if (!affectedUser) { - const { uid } = await matrixClient.user.createLocal(affectedMatrixUserId); - - affectedUser = Users.findOneById(uid); - } - - if (!roomId && fromADifferentServer) { - roomId = await processFirstAccessFromExternalServer( - matrixRoomId, - senderMatrixUserId, - affectedMatrixUserId, - senderUser, - affectedUser, - isDirect, - roomState as IMatrixEvent[], - ); - } - - if (!roomId) { - return; - } - - switch (membership) { - case AddMemberToRoomMembership.JOIN: - await addUserToRoomAsync(roomId, affectedUser); - break; - case AddMemberToRoomMembership.INVITE: - // TODO: this should be a local invite - await addUserToRoomAsync(roomId, affectedUser, senderUser); - break; - case AddMemberToRoomMembership.LEAVE: - await removeUserFromRoom(roomId, affectedUser, { - byUser: senderUser, - }); - break; - } -}; diff --git a/apps/meteor/app/federation-v2/server/events/sendMessage.ts b/apps/meteor/app/federation-v2/server/events/sendMessage.ts deleted file mode 100644 index c70577d1e2af..000000000000 --- a/apps/meteor/app/federation-v2/server/events/sendMessage.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { sendMessage } from '../../../lib/server'; -import { Rooms } from '../../../models/server/raw'; - -export const sendMessageAsync = async (user: any, msg: any, room: any): Promise => - new Promise((resolve) => resolve(sendMessage(user, msg, room))); - -export const handleSendMessage = async (event: IMatrixEvent): Promise => { - const { room_id: matrixRoomId, sender } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - // Find the bridged user id - const userId = await MatrixBridgedUser.getId(sender); - - // Find the user - const user = await Users.findOneById(userId); - - const room = await Rooms.findOneById(roomId); - - await sendMessageAsync(user, { msg: event.content.body }, room); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts deleted file mode 100644 index e95bf691bf43..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; - -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; -import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; - -export const setRoomJoinRules = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { join_rule: joinRule }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - if (!roomId) { - return; - } - - const localRoom = await Rooms.findOneById(roomId); - - if (!localRoom || localRoom?.t === RoomType.DIRECT_MESSAGE) { - return; - } - - let type; - - switch (joinRule) { - case SetRoomJoinRules.INVITE: - type = RoomType.PRIVATE_GROUP; - break; - case SetRoomJoinRules.JOIN: - default: - type = RoomType.CHANNEL; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - t: type, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - t: type, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomName.ts b/apps/meteor/app/federation-v2/server/events/setRoomName.ts deleted file mode 100644 index 243791841d70..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomName.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Rooms, Subscriptions } from '../../../models/server/raw'; -import { MatrixBridgedRoom } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomName = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { name }, - } = event; - - // Normalize room name - const normalizedName = name.replace('@', ''); - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - if (!roomId) { - return; - } - - await Rooms.update( - { _id: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - ); - - await Subscriptions.update( - { rid: roomId }, - { - $set: { - name: normalizedName, - fname: normalizedName, - }, - }, - { multi: true }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts b/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts deleted file mode 100644 index d75d38df651e..000000000000 --- a/apps/meteor/app/federation-v2/server/events/setRoomTopic.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MatrixBridgedRoom, Rooms } from '../../../models/server'; -import { IMatrixEvent } from '../definitions/IMatrixEvent'; -import { MatrixEventType } from '../definitions/MatrixEventType'; - -export const setRoomTopic = async (event: IMatrixEvent): Promise => { - const { - room_id: matrixRoomId, - content: { topic }, - } = event; - - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); - - Rooms.update( - { _id: roomId }, - { - $set: { - description: topic, - }, - }, - ); -}; diff --git a/apps/meteor/app/federation-v2/server/index.ts b/apps/meteor/app/federation-v2/server/index.ts index e331badfd006..d2d90966d4d0 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,4 +1,35 @@ -import './settings'; -import { startBridge } from './startup'; +import { FederationFactory } from './infrastructure/Factory'; -startBridge(); +const PROCESSING_CONCURRENCY = 1; + +const rocketSettingsAdapter = FederationFactory.buildRocketSettingsAdapter(); +rocketSettingsAdapter.initialize(); +const queueInstance = FederationFactory.buildQueue(); +const federation = FederationFactory.buildBridge(rocketSettingsAdapter, queueInstance); +const rocketRoomAdapter = FederationFactory.buildRocketRoomAdapter(); +const rocketUserAdapter = FederationFactory.buildRocketUserAdapter(); +const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); +const rocketNotificationAdapter = FederationFactory.buildRocketNotificationdapter(); + +const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + federation, +); +const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver); + +export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketNotificationAdapter, + federation, +); + +(async (): Promise => { + queueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), PROCESSING_CONCURRENCY); + await federation.start(); + await rocketSettingsAdapter.onFederationEnabledStatusChanged(federation.onFederationAvailabilityChanged.bind(federation)); +})(); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts new file mode 100644 index 000000000000..36a20f827c10 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -0,0 +1,90 @@ +import { FederationRoomServiceReceiver } from '../application/RoomServiceReceiver'; +import { FederationRoomServiceSender } from '../application/RoomServiceSender'; +import { MatrixBridge } from './matrix/Bridge'; +import { MatrixEventsHandler } from './matrix/handlers'; +import { + MatrixRoomCreatedHandler, + MatrixRoomJoinRulesChangedHandler, + MatrixRoomMembershipChangedHandler, + MatrixRoomMessageSentHandler, + MatrixRoomNameChangedHandler, + MatrixRoomTopicChangedHandler, +} from './matrix/handlers/Room'; +import { InMemoryQueue } from './queue/InMemoryQueue'; +import { RocketChatMessageAdapter } from './rocket-chat/adapters/Message'; +import { RocketChatRoomAdapter } from './rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from './rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from './rocket-chat/adapters/User'; +import { IFederationBridge } from '../domain/IFederationBridge'; +import { RocketChatNotificationAdapter } from './rocket-chat/adapters/Notification'; + +export class FederationFactory { + public static buildRocketSettingsAdapter(): RocketChatSettingsAdapter { + return new RocketChatSettingsAdapter(); + } + + public static buildRocketRoomAdapter(): RocketChatRoomAdapter { + return new RocketChatRoomAdapter(); + } + + public static buildRocketUserAdapter(): RocketChatUserAdapter { + return new RocketChatUserAdapter(); + } + + public static buildRocketMessageAdapter(): RocketChatMessageAdapter { + return new RocketChatMessageAdapter(); + } + + public static buildRocketNotificationdapter(): RocketChatNotificationAdapter { + return new RocketChatNotificationAdapter(); + } + + public static buildQueue(): InMemoryQueue { + return new InMemoryQueue(); + } + + public static buildRoomServiceReceiver( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceReceiver { + return new FederationRoomServiceReceiver(rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, bridge); + } + + public static buildRoomServiceSender( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketNotificationAdapter: RocketChatNotificationAdapter, + bridge: IFederationBridge, + ): FederationRoomServiceSender { + return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketNotificationAdapter, bridge); + } + + public static buildBridge(rocketSettingsAdapter: RocketChatSettingsAdapter, queue: InMemoryQueue): IFederationBridge { + return new MatrixBridge( + rocketSettingsAdapter.getApplicationServiceId(), + rocketSettingsAdapter.getHomeServerUrl(), + rocketSettingsAdapter.getHomeServerDomain(), + rocketSettingsAdapter.getBridgeUrl(), + rocketSettingsAdapter.getBridgePort(), + rocketSettingsAdapter.generateRegistrationFileObject(), + queue.addToQueue.bind(queue), + ); + } + + public static buildEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): MatrixEventsHandler { + const EVENT_HANDLERS = [ + new MatrixRoomCreatedHandler(roomServiceReceive), + new MatrixRoomMembershipChangedHandler(roomServiceReceive), + new MatrixRoomJoinRulesChangedHandler(roomServiceReceive), + new MatrixRoomNameChangedHandler(roomServiceReceive), + new MatrixRoomTopicChangedHandler(roomServiceReceive), + new MatrixRoomMessageSentHandler(roomServiceReceive), + ]; + + return new MatrixEventsHandler(EVENT_HANDLERS); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts new file mode 100644 index 000000000000..acb2824f6903 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -0,0 +1,177 @@ +import { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { IFederationBridge } from '../../domain/IFederationBridge'; +import { bridgeLogger } from '../rocket-chat/adapters/logger'; +import { IMatrixEvent } from './definitions/IMatrixEvent'; +import { MatrixEventType } from './definitions/MatrixEventType'; + +export class MatrixBridge implements IFederationBridge { + private bridgeInstance: Bridge; + + private isRunning = false; + + constructor( + private appServiceId: string, + private homeServerUrl: string, + private homeServerDomain: string, + private bridgeUrl: string, + private bridgePort: number, + private homeServerRegistrationFile: Record, + private eventHandler: Function, + ) { + this.logInfo(); + } + + public async onFederationAvailabilityChanged(enabled: boolean): Promise { + if (!enabled) { + await this.stop(); + return; + } + await this.start(); + } + + public async start(): Promise { + try { + await this.stop(); + await this.createInstance(); + + if (!this.isRunning) { + await this.bridgeInstance.run(this.bridgePort); + this.isRunning = true; + } + } 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 { + if (!this.isRunning) { + return; + } + // 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 { + try { + return this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId); + } catch (err) { + // no-op + } + } + + public async joinRoom(externalRoomId: string, externalUserId: string): Promise { + await this.bridgeInstance.getIntent(externalUserId).join(externalRoomId); + } + + public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise { + await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + } + + public async createUser(username: string, name: string, domain: string): Promise { + const matrixUserId = `@${username?.toLowerCase()}:${domain}`; + const intent = this.bridgeInstance.getIntent(matrixUserId); + + await intent.ensureProfile(name); + await intent.setDisplayName(`${username} (${name})`); + + return matrixUserId; + } + + public async createRoom( + externalCreatorId: string, + externalInviteeId: string, + roomType: RoomType, + roomName: string, + roomTopic?: string, + ): Promise { + const intent = this.bridgeInstance.getIntent(externalCreatorId); + + const visibility = roomType === 'p' || roomType === 'd' ? 'invite' : 'public'; + const preset = roomType === 'p' || roomType === 'd' ? 'private_chat' : 'public_chat'; + + // Create the matrix room + const matrixRoom = await intent.createRoom({ + createAsClient: true, + options: { + name: roomName, + topic: roomTopic, + visibility, + preset, + ...this.parametersForDirectMessagesIfNecessary(roomType, externalInviteeId), + // 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; + } + + public async sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise { + await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, text); + } + + public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean { + const userDomain = externalUserId.includes(':') ? externalUserId.split(':').pop() : ''; + + return userDomain === domain; + } + + public getInstance(): IFederationBridge { + return this; + } + + private parametersForDirectMessagesIfNecessary = (roomType: RoomType, invitedUserId: string): Record => { + return roomType === RoomType.DIRECT_MESSAGE + ? { + // eslint-disable-next-line @typescript-eslint/camelcase + is_direct: true, + invite: [invitedUserId], + } + : {}; + }; + + private logInfo(): void { + bridgeLogger.info(`Running Federation V2: + id: ${this.appServiceId} + bridgeUrl: ${this.bridgeUrl} + homeserverURL: ${this.homeServerUrl} + homeserverDomain: ${this.homeServerDomain} + `); + } + + private async createInstance(): Promise { + bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); + + // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails + const { Bridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); + + this.bridgeInstance = new Bridge({ + homeserverUrl: this.homeServerUrl, + domain: this.homeServerDomain, + registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput), + disableStores: true, + controller: { + onAliasQuery: (alias, matrixRoomId): void => { + console.log('onAliasQuery', alias, matrixRoomId); + }, + onEvent: async (request /* , context*/): Promise => { + // Get the event + const event = request.getData() as unknown as IMatrixEvent; + this.eventHandler(event); + }, + onLog: async (line, isError): Promise => { + console.log(line, isError); + }, + }, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts new file mode 100644 index 000000000000..9d845516cd75 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -0,0 +1,152 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { + FederationRoomChangeJoinRulesDto, + FederationRoomChangeMembershipDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, + FederationRoomCreateInputDto, + FederationRoomSendInternalMessageDto, +} from '../../../application/input/RoomReceiverDto'; +import { EVENT_ORIGIN } from '../../../domain/IFederationBridge'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { RoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export class MatrixRoomReceiverConverter { + public static toRoomCreateDto(externalEvent: IMatrixEvent): FederationRoomCreateInputDto { + return Object.assign(new FederationRoomCreateInputDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + wasInternallyProgramaticallyCreated: externalEvent.content?.was_internally_programatically_created || false, + }); + } + + public static toChangeRoomMembershipDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeMembershipDto { + return Object.assign(new FederationRoomChangeMembershipDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + ...MatrixRoomReceiverConverter.tryToGetExternalInfoFromTheRoomState( + externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state, + externalEvent.content?.is_direct, + ), + externalInviterId: externalEvent.sender, + normalizedInviterId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + externalInviteeId: externalEvent.state_key, + 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), + leave: externalEvent.content?.membership === AddMemberToRoomMembership.LEAVE, + }); + } + + public static toSendRoomMessageDto(externalEvent: IMatrixEvent): FederationRoomSendInternalMessageDto { + return Object.assign(new FederationRoomSendInternalMessageDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + externalSenderId: externalEvent.sender, + normalizedSenderId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.sender), + text: externalEvent.content?.body, + }); + } + + public static toRoomChangeJoinRulesDto( + externalEvent: IMatrixEvent, + ): FederationRoomChangeJoinRulesDto { + return Object.assign(new FederationRoomChangeJoinRulesDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalEvent.content?.join_rule), + }); + } + + public static toRoomChangeNameDto(externalEvent: IMatrixEvent): FederationRoomChangeNameDto { + return Object.assign(new FederationRoomChangeNameDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + normalizedRoomName: MatrixRoomReceiverConverter.normalizeRoomNameToRCFormat(externalEvent.content?.name), + }); + } + + public static toRoomChangeTopicDto(externalEvent: IMatrixEvent): FederationRoomChangeTopicDto { + return Object.assign(new FederationRoomChangeTopicDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + roomTopic: externalEvent.content?.topic, + }); + } + + private static convertMatrixUserIdFormatToRCFormat(matrixUserId = ''): string { + return matrixUserId.replace('@', ''); + } + + private static convertMatrixRoomIdFormatToRCFormat(matrixRoomId = ''): string { + const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; + const prefix = '!'; + + return prefixedRoomIdOnly?.replace(prefix, ''); + } + + private static normalizeRoomNameToRCFormat(matrixRoomName = ''): string { + return matrixRoomName.replace('@', ''); + } + + private static formatMatrixUserIdToRCUsernameFormat(matrixUserId = ''): string { + return matrixUserId.split(':')[0]?.replace('@', ''); + } + + private static getEventOrigin(inviterId = '', inviteeId = ''): EVENT_ORIGIN { + const fromADifferentServer = + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== + MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviteeId); + + return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL; + } + + private static extractServerNameFromMatrixUserId(matrixUserId = ''): string { + const splitted = matrixUserId.split(':'); + + return splitted.length > 1 ? splitted[1] : ''; + } + + private static getBasicRoomsFields(externalRoomId: string): Record { + return { + externalRoomId, + normalizedRoomId: MatrixRoomReceiverConverter.convertMatrixRoomIdFormatToRCFormat(externalRoomId), + }; + } + + private static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType { + const mapping: Record = { + [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; + } + + private static tryToGetExternalInfoFromTheRoomState( + roomState: Record[] = [], + matrixRoomIsDirect = false, + ): Record { + if (roomState.length === 0) { + return {}; + } + const externalRoomName = roomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_NAME_CHANGED) + ?.content?.name; + const externalRoomJoinRule = roomState.find( + (stateEvent: Record) => stateEvent.type === MatrixEventType.ROOM_JOIN_RULES_CHANGED, + )?.content?.join_rule; + + return { + ...(externalRoomName ? { externalRoomName } : {}), + ...(externalRoomJoinRule + ? { roomType: MatrixRoomReceiverConverter.convertMatrixJoinRuleToRCRoomType(externalRoomJoinRule, matrixRoomIsDirect) } + : {}), + }; + } +} diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts similarity index 84% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts index 7111057ec55e..3470ab6481bc 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEvent.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEvent.ts @@ -11,6 +11,6 @@ export interface IMatrixEvent { sender: string; state_key: string; type: T; - unsigned: { age: number }; + unsigned: { age: number; invite_room_state: Record[] }; user_id: string; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts similarity index 64% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts index 6180c0356200..f9e80f615808 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts @@ -1,5 +1,5 @@ export interface IMatrixEventContentCreateRoom { creator: string; room_version: string; - was_programatically_created?: boolean; + was_internally_programatically_created?: boolean; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSendMessage.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts similarity index 61% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts index 920f9bb53777..f97fa09d8aa7 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts @@ -1,8 +1,8 @@ -export enum SetRoomJoinRules { +export enum RoomJoinRules { JOIN = 'public', INVITE = 'invite', } export interface IMatrixEventContentSetRoomJoinRules { - join_rule: SetRoomJoinRules; + join_rule: RoomJoinRules; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomName.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts similarity index 100% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomTopic.ts diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts similarity index 57% rename from apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts index 7615779282e1..3c1d5b52f076 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts @@ -7,10 +7,10 @@ import { IMatrixEventContentSetRoomName } from './IMatrixEventContentSetRoomName import { IMatrixEventContentSetRoomTopic } from './IMatrixEventContentSetRoomTopic'; export type EventContent = { - [MatrixEventType.CREATE_ROOM]: IMatrixEventContentCreateRoom; - [MatrixEventType.ROOM_MEMBERSHIP]: IMatrixEventContentAddMemberToRoom; - [MatrixEventType.SET_ROOM_JOIN_RULES]: IMatrixEventContentSetRoomJoinRules; - [MatrixEventType.SET_ROOM_NAME]: IMatrixEventContentSetRoomName; - [MatrixEventType.SET_ROOM_TOPIC]: IMatrixEventContentSetRoomTopic; - [MatrixEventType.SEND_MESSAGE]: IMatrixEventContentSendMessage; + [MatrixEventType.ROOM_CREATED]: IMatrixEventContentCreateRoom; + [MatrixEventType.ROOM_MEMBERSHIP_CHANGED]: IMatrixEventContentAddMemberToRoom; + [MatrixEventType.ROOM_JOIN_RULES_CHANGED]: IMatrixEventContentSetRoomJoinRules; + [MatrixEventType.ROOM_NAME_CHANGED]: IMatrixEventContentSetRoomName; + [MatrixEventType.ROOM_TOPIC_CHANGED]: IMatrixEventContentSetRoomTopic; + [MatrixEventType.ROOM_MESSAGE_SENT]: IMatrixEventContentSendMessage; }; diff --git a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts similarity index 51% rename from apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts rename to apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts index 14d4f0bb0ecb..bb58a0d71825 100644 --- a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts @@ -1,12 +1,12 @@ export enum MatrixEventType { - CREATE_ROOM = 'm.room.create', - ROOM_MEMBERSHIP = 'm.room.member', + ROOM_CREATED = 'm.room.create', + ROOM_MEMBERSHIP_CHANGED = 'm.room.member', // SET_ROOM_POWER_LEVELS = 'm.room.power_levels', // SET_ROOM_CANONICAL_ALIAS = 'm.room.canonical_alias', - SET_ROOM_JOIN_RULES = 'm.room.join_rules', + ROOM_JOIN_RULES_CHANGED = 'm.room.join_rules', // SET_ROOM_HISTORY_VISIBILITY = 'm.room.history_visibility', // SET_ROOM_GUEST_ACCESS = 'm.room.guest_access', - SET_ROOM_NAME = 'm.room.name', - SET_ROOM_TOPIC = 'm.room.topic', - SEND_MESSAGE = 'm.room.message', + ROOM_NAME_CHANGED = 'm.room.name', + ROOM_TOPIC_CHANGED = 'm.room.topic', + ROOM_MESSAGE_SENT = 'm.room.message', } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts new file mode 100644 index 000000000000..25e179bfae7e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; + +export abstract class MatrixBaseEventHandler { + private type: T; + + public abstract handle(externalEvent: IMatrixEvent): Promise; + + protected constructor(type: T) { + this.type = type; + } + + public equals(type: MatrixEventType): boolean { + return this.type === type; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts new file mode 100644 index 000000000000..47d10be8fb73 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -0,0 +1,65 @@ +import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver'; +import { MatrixRoomReceiverConverter } from '../converters/RoomReceiver'; +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_CREATED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.createRoom(MatrixRoomReceiverConverter.toRoomCreateDto(externalEvent)); + } +} + +export class MatrixRoomMembershipChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomMembership(MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent)); + } +} + +export class MatrixRoomJoinRulesChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_JOIN_RULES_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeJoinRules(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(externalEvent)); + } +} + +export class MatrixRoomNameChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_NAME_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomName(MatrixRoomReceiverConverter.toRoomChangeNameDto(externalEvent)); + } +} + +export class MatrixRoomTopicChangedHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_TOPIC_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomTopic(MatrixRoomReceiverConverter.toRoomChangeTopicDto(externalEvent)); + } +} + +export class MatrixRoomMessageSentHandler extends MatrixBaseEventHandler { + constructor(private roomService: FederationRoomServiceReceiver) { + super(MatrixEventType.ROOM_MESSAGE_SENT); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.receiveExternalMessage(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts new file mode 100644 index 000000000000..67b361681492 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts @@ -0,0 +1,16 @@ +import { IMatrixEvent } from '../definitions/IMatrixEvent'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixEventsHandler { + // eslint-disable-next-line no-empty-function + constructor(private handlers: MatrixBaseEventHandler[]) {} + + public async handleEvent(event: IMatrixEvent): Promise { + const handler = this.handlers.find((handler) => handler.equals(event.type)); + if (!handler) { + return console.log(`Could not find handler for ${event.type}`, event); + } + return handler?.handle(event); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts new file mode 100644 index 000000000000..fc4ea1106d26 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/queue/InMemoryQueue.ts @@ -0,0 +1,16 @@ +import * as fastq from 'fastq'; + +export class InMemoryQueue { + private instance: any; + + public setHandler(handler: Function, concurrency: number): void { + this.instance = fastq.promise(handler as any, concurrency); + } + + public addToQueue(task: Record): void { + if (!this.instance) { + throw new Error('You need to set the handler first'); + } + this.instance.push(task).catch(console.error); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts new file mode 100644 index 000000000000..3f82f77a6a19 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts @@ -0,0 +1,9 @@ +import { sendMessage } from '../../../../../lib/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatMessageAdapter { + public async sendMessage(user: FederatedUser, text: string, room: FederatedRoom): Promise { + new Promise((resolve) => resolve(sendMessage(user.internalReference, { msg: text }, room.internalReference))); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts new file mode 100644 index 000000000000..8e07d94cf12a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Notification.ts @@ -0,0 +1,14 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { api } from '../../../../../../server/sdk/api'; + +export class RocketChatNotificationAdapter { + public notifyWithEphemeralMessage(i18nMessageKey: string, userId: string, roomId: string, language = 'en'): void { + api.broadcast('notify.ephemeralMessage', userId, roomId, { + msg: TAPi18n.__(i18nMessageKey, { + postProcess: 'sprintf', + lng: language, + }), + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts new file mode 100644 index 000000000000..44ec3ee5e5ad --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts @@ -0,0 +1,103 @@ +import { ICreatedRoom, IRoom } from '@rocket.chat/core-typings'; + +import { MatrixBridgedRoom } from '../../../../../models/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { createRoom, addUserToRoom, removeUserFromRoom } from '../../../../../lib/server'; +import { Rooms, Subscriptions } from '../../../../../models/server/raw'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatRoomAdapter { + public async getFederatedRoomByExternalId(externalRoomId: string): Promise { + const internalBridgedRoomId = MatrixBridgedRoom.getId(externalRoomId); + if (!internalBridgedRoomId) { + return; + } + const room = await Rooms.findOneById(internalBridgedRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getFederatedRoomByInternalId(internalRoomId: string): Promise { + const externalRoomId = MatrixBridgedRoom.getMatrixId(internalRoomId); + if (!externalRoomId) { + return; + } + const room = await Rooms.findOneById(internalRoomId); + + return this.createFederatedRoomInstance(externalRoomId, room); + } + + public async getInternalRoomById(internalRoomId: string): Promise { + return Rooms.findOneById(internalRoomId); + } + + public async createFederatedRoom(federatedRoom: FederatedRoom): Promise { + const members = federatedRoom.getMembers(); + const { rid, _id } = createRoom( + federatedRoom.internalReference.t, + federatedRoom.internalReference.name, + federatedRoom.internalReference.u.username as string, + members, + ) as ICreatedRoom; + const roomId = rid || _id; + MatrixBridgedRoom.insert({ rid: roomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(roomId); + } + + public async updateFederatedRoomByInternalRoomId(internalRoomId: string, federatedRoom: FederatedRoom): Promise { + MatrixBridgedRoom.upsert({ rid: internalRoomId }, { rid: internalRoomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(internalRoomId); + } + + public async addUserToRoom(federatedRoom: FederatedRoom, inviteeUser: FederatedUser, inviterUser?: FederatedUser): Promise { + return new Promise((resolve) => + resolve(addUserToRoom(federatedRoom.internalReference._id, inviteeUser.internalReference, inviterUser?.internalReference) as any), + ); + } + + public async removeUserFromRoom(federatedRoom: FederatedRoom, affectedUser: FederatedUser, byUser: FederatedUser): Promise { + return new Promise((resolve) => + resolve( + removeUserFromRoom(federatedRoom.internalReference._id, affectedUser.internalReference, { + byUser: byUser.internalReference, + }) as any, + ), + ); + } + + public async updateRoomType(federatedRoom: FederatedRoom): Promise { + await Rooms.update({ _id: federatedRoom.internalReference._id }, { $set: { t: federatedRoom.internalReference.t } }); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { t: federatedRoom.internalReference.t } }, + { multi: true }, + ); + } + + public async updateRoomName(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + ); + await Subscriptions.update( + { rid: federatedRoom.internalReference._id }, + { $set: { name: federatedRoom.internalReference.name, fname: federatedRoom.internalReference.fname } }, + { multi: true }, + ); + } + + public async updateRoomTopic(federatedRoom: FederatedRoom): Promise { + await Rooms.update( + { _id: federatedRoom.internalReference._id }, + { $set: { description: federatedRoom.internalReference.description } }, + ); + } + + private createFederatedRoomInstance(externalRoomId: string, room: IRoom): FederatedRoom { + const federatedRoom = FederatedRoom.build(); + federatedRoom.externalId = externalRoomId; + federatedRoom.internalReference = room; + + return federatedRoom; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts new file mode 100644 index 000000000000..71ff9af73461 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts @@ -0,0 +1,192 @@ +import yaml from 'js-yaml'; +import { SHA256 } from 'meteor/sha'; + +import { Settings } from '../../../../../models/server/raw'; +import { settings, settingsRegistry } from '../../../../../settings/server'; + +const EVERYTHING_REGEX = '.*'; +const LISTEN_RULES = EVERYTHING_REGEX; + +export class RocketChatSettingsAdapter { + public initialize(): void { + this.addFederationSettings(); + this.watchChangesAndUpdateRegistrationFile(); + } + + public getApplicationServiceId(): string { + return settings.get('Federation_Matrix_id'); + } + + public getApplicationHomeServerToken(): string { + return settings.get('Federation_Matrix_hs_token'); + } + + public getApplicationApplicationServiceToken(): string { + return settings.get('Federation_Matrix_as_token'); + } + + public getBridgeUrl(): string { + return settings.get('Federation_Matrix_bridge_url'); + } + + public getBridgePort(): number { + const [, , port] = this.getBridgeUrl().split(':'); + + return parseInt(port); + } + + public getHomeServerUrl(): string { + return settings.get('Federation_Matrix_homeserver_url'); + } + + public getHomeServerDomain(): string { + return settings.get('Federation_Matrix_homeserver_domain'); + } + + public getBridgeBotUsername(): string { + return settings.get('Federation_Matrix_bridge_localpart'); + } + + public async disableFederation(): Promise { + await Settings.updateValueById('Federation_Matrix_enabled', false); + } + + public onFederationEnabledStatusChanged(callback: Function): void { + settings.watchMultiple( + [ + 'Federation_Matrix_enabled', + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + ([enabled]) => callback(enabled), + ); + } + + public generateRegistrationFileObject(): Record { + /* eslint-disable @typescript-eslint/camelcase */ + return { + id: this.getApplicationServiceId(), + hs_token: this.getApplicationHomeServerToken(), + as_token: this.getApplicationApplicationServiceToken(), + url: this.getBridgeUrl(), + sender_localpart: this.getBridgeBotUsername(), + namespaces: { + users: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + rooms: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + aliases: [ + { + exclusive: false, + regex: LISTEN_RULES, + }, + ], + }, + }; + /* eslint-enable @typescript-eslint/camelcase */ + } + + private async updateRegistrationFile(): Promise { + await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(this.generateRegistrationFileObject())); + } + + private watchChangesAndUpdateRegistrationFile(): void { + settings.watchMultiple( + [ + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + this.updateRegistrationFile.bind(this), + ); + } + + private addFederationSettings(): void { + settingsRegistry.addGroup('Federation', function () { + this.section('Matrix Bridge', function () { + this.add('Federation_Matrix_enabled', false, { + readonly: false, + type: 'boolean', + i18nLabel: 'Federation_Matrix_enabled', + i18nDescription: 'Federation_Matrix_enabled_desc', + alert: 'Federation_Matrix_Enabled_Alert', + }); + + const uniqueId = settings.get('uniqueID'); + const hsToken = SHA256(`hs_${uniqueId}`); + const asToken = SHA256(`as_${uniqueId}`); + + this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_id', + i18nDescription: 'Federation_Matrix_id_desc', + }); + + this.add('Federation_Matrix_hs_token', hsToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_hs_token', + i18nDescription: 'Federation_Matrix_hs_token_desc', + }); + + this.add('Federation_Matrix_as_token', asToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_as_token', + i18nDescription: 'Federation_Matrix_as_token_desc', + }); + + this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_url', + i18nDescription: 'Federation_Matrix_homeserver_url_desc', + alert: 'Federation_Matrix_homeserver_url_alert', + }); + + this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_domain', + i18nDescription: 'Federation_Matrix_homeserver_domain_desc', + alert: 'Federation_Matrix_homeserver_domain_alert', + }); + + this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_url', + i18nDescription: 'Federation_Matrix_bridge_url_desc', + }); + + this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_localpart', + i18nDescription: 'Federation_Matrix_bridge_localpart_desc', + }); + + this.add('Federation_Matrix_registration_file', '', { + readonly: true, + type: 'code', + i18nLabel: 'Federation_Matrix_registration_file', + i18nDescription: 'Federation_Matrix_registration_file_desc', + }); + }); + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts new file mode 100644 index 000000000000..a2f2c26f8e6c --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts @@ -0,0 +1,85 @@ +import { IUser } from '@rocket.chat/core-typings'; + +import { MatrixBridgedUser, Users } from '../../../../../models/server'; +import { FederatedUser } from '../../../domain/FederatedUser'; + +export class RocketChatUserAdapter { + public async getFederatedUserByExternalId(externalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getId(externalUserId); + if (!internalBridgedUserId) { + return; + } + + const user = await Users.findOneById(internalBridgedUserId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalId(internalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getById(internalUserId); + if (!internalBridgedUserId) { + return; + } + const { uid: userId, mui: externalUserId } = internalBridgedUserId; + const user = await Users.findOneById(userId); + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getFederatedUserByInternalUsername(username: string): Promise { + const user = await Users.findOneByUsername(username); + if (!user) { + return; + } + const internalBridgedUserId = MatrixBridgedUser.getById(user._id); + if (!internalBridgedUserId) { + return; + } + const { mui: externalUserId } = internalBridgedUserId; + + return this.createFederatedUserInstance(externalUserId, user); + } + + public async getInternalUserById(userId: string): Promise { + return Users.findOneById(userId); + } + + public async createFederatedUser(federatedUser: FederatedUser): Promise { + const existingLocalUser = await Users.findOneByUsername(federatedUser.internalReference.username); + if (existingLocalUser) { + return MatrixBridgedUser.upsert( + { uid: existingLocalUser._id }, + { + uid: existingLocalUser._id, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + const newLocalUserId = await Users.create({ + username: federatedUser.internalReference.username, + type: federatedUser.internalReference.type, + status: federatedUser.internalReference.status, + active: federatedUser.internalReference.active, + roles: federatedUser.internalReference.roles, + name: federatedUser.internalReference.name, + requirePasswordChange: federatedUser.internalReference.requirePasswordChange, + }); + MatrixBridgedUser.upsert( + { uid: newLocalUserId }, + { + uid: newLocalUserId, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + + private createFederatedUserInstance(externalUserId: string, user: IUser): FederatedUser { + const federatedUser = FederatedUser.build(); + federatedUser.externalId = externalUserId; + federatedUser.internalReference = user; + + return federatedUser; + } +} diff --git a/apps/meteor/app/federation-v2/server/logger.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts similarity index 73% rename from apps/meteor/app/federation-v2/server/logger.ts rename to apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts index 0b88f48bfde6..331ed9f5f4b4 100644 --- a/apps/meteor/app/federation-v2/server/logger.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts @@ -1,4 +1,4 @@ -import { Logger } from '../../logger/server'; +import { Logger } from '../../../../../logger/server'; const logger = new Logger('Federation_Matrix'); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts new file mode 100644 index 000000000000..76d6937f206b --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts @@ -0,0 +1,34 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomInviteUserDto, FederationRoomSendExternalMessageDto } from '../../../application/input/RoomSenderDto'; + +export class FederationRoomSenderConverter { + public static toRoomInviteUserDto( + internalInviterId: string, + internalRoomId: string, + externalInviteeId: string, + ): FederationRoomInviteUserDto { + const normalizedInviteeId = externalInviteeId.replace('@', ''); + const inviteeUsernameOnly = externalInviteeId.split(':')[0]?.replace('@', ''); + + return Object.assign(new FederationRoomInviteUserDto(), { + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId, + inviteeUsernameOnly, + }); + } + + public static toSendExternalMessageDto( + internalSenderId: string, + internalRoomId: string, + message: IMessage, + ): FederationRoomSendExternalMessageDto { + return Object.assign(new FederationRoomSendExternalMessageDto(), { + internalRoomId, + internalSenderId, + message, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/matrix-client/index.ts b/apps/meteor/app/federation-v2/server/matrix-client/index.ts deleted file mode 100644 index 68664d6e9cdf..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as message from './message'; -import * as room from './room'; -import * as user from './user'; - -export const matrixClient = { - message, - room, - user, -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/message.ts b/apps/meteor/app/federation-v2/server/matrix-client/message.ts deleted file mode 100644 index a6a9d8626632..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/message.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IMessage } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; - -export const send = async (message: IMessage): Promise => { - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(message.u._id); - - // Retrieve the matrix room - const roomMatrixId = MatrixBridgedRoom.getMatrixId(message.rid); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${message.u._id}`); - } - - if (!roomMatrixId) { - throw new Error(`Could not find room matrix id for ${message.rid}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - await intent.sendText(roomMatrixId, message.msg || '...not-supported...'); - - return message; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/room.ts b/apps/meteor/app/federation-v2/server/matrix-client/room.ts deleted file mode 100644 index e031de4e4b2b..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/room.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { MatrixBridgedRoom, MatrixBridgedUser } from '../../../models/server'; -import { matrixBridge } from '../bridge'; -import { Rooms } from '../../../models/server/raw'; - -interface ICreateRoomResult { - rid: string; - mri: string; -} - -const parametersForDirectMessagesIfNecessary = (room: IRoom, invitedUserId: string): Record => { - return room.t === RoomType.DIRECT_MESSAGE - ? { - // eslint-disable-next-line @typescript-eslint/camelcase - is_direct: true, - invite: [invitedUserId], - } - : {}; -}; - -export const create = async (inviterUser: IUser, room: IRoom, invitedUserId: string): Promise => { - // Check if this room already exists (created by another method) - // and if so, ignore the callback - const roomMatrixId = MatrixBridgedRoom.getMatrixId(room._id); - if (roomMatrixId) { - return { rid: room._id, mri: roomMatrixId }; - } - - // Retrieve the matrix user - const userMatrixId = MatrixBridgedUser.getMatrixId(inviterUser._id); - - if (!userMatrixId) { - throw new Error(`Could not find user matrix id for ${inviterUser._id}`); - } - - const intent = matrixBridge.getInstance().getIntent(userMatrixId); - - const visibility = room.t === 'p' || room.t === 'd' ? 'invite' : 'public'; - const preset = room.t === 'p' || room.t === 'd' ? 'private_chat' : 'public_chat'; - - // Create the matrix room - const matrixRoom = await intent.createRoom({ - createAsClient: true, - options: { - name: room.fname || room.name, - topic: room.topic, - visibility, - preset, - ...parametersForDirectMessagesIfNecessary(room, invitedUserId), - // eslint-disable-next-line @typescript-eslint/camelcase - creation_content: { - // eslint-disable-next-line @typescript-eslint/camelcase - was_programatically_created: true, - }, - }, - }); - // Add to the map - MatrixBridgedRoom.insert({ rid: room._id, mri: matrixRoom.room_id }); - - await Rooms.setAsBridged(room._id); - - // Add our user TODO: Doing this I think is un-needed since our user is the creator of the room. With it in.. there were errors - // await intent.invite(matrixRoom.room_id, userMatrixId); - - return { rid: room._id, mri: matrixRoom.room_id }; -}; diff --git a/apps/meteor/app/federation-v2/server/matrix-client/user.ts b/apps/meteor/app/federation-v2/server/matrix-client/user.ts deleted file mode 100644 index 9e6ba092e9b4..000000000000 --- a/apps/meteor/app/federation-v2/server/matrix-client/user.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { MatrixProfileInfo } from '@rocket.chat/forked-matrix-bot-sdk'; -import { IUser } from '@rocket.chat/core-typings'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { matrixBridge } from '../bridge'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; -import { addUserToRoom } from '../../../lib/server/functions'; -import { matrixClient } from '.'; -import { dataInterface } from '../data-interface'; -import { settings } from '../../../settings/server'; -import { api } from '../../../../server/sdk/api'; - -interface ICreateUserResult { - uid: string; - mui: string; - remote: boolean; -} - -const removeUselessCharsFromMatrixId = (matrixUserId = ''): string => matrixUserId.replace('@', ''); -const formatUserIdAsRCUsername = (userId = ''): string => removeUselessCharsFromMatrixId(userId.split(':')[0]); - -export const invite = async (inviterId: string, roomId: string, invitedId: string): Promise => { - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting user ${invitedId} to ${roomId}...`); - - // Find the inviter user - let bridgedInviterUser = MatrixBridgedUser.getById(inviterId); - // Get the user - const inviterUser = await dataInterface.user(inviterId); - - // Determine if the user is local or remote - let invitedUserMatrixId = invitedId; - const invitedUserDomain = invitedId.includes(':') ? invitedId.split(':').pop() : ''; - const invitedUserIsRemote = invitedUserDomain && invitedUserDomain !== settings.get('Federation_Matrix_homeserver_domain'); - - // Find the invited user in Rocket.Chats users - // TODO: this should be refactored asap, since these variable value changes lead us to confusion - let invitedUser = Users.findOneByUsername(removeUselessCharsFromMatrixId(invitedId)); - - if (!invitedUser) { - // Create the invited user - const { uid } = await matrixClient.user.createLocal(invitedUserMatrixId); - invitedUser = Users.findOneById(uid); - } - - // The inviters user doesn't yet exist in matrix - if (!bridgedInviterUser) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote inviter user...`); - - // Create the missing user - bridgedInviterUser = await matrixClient.user.createRemote(inviterUser); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviter user created as ${bridgedInviterUser.mui}...`); - } - - // Find the bridged room id - let matrixRoomId = await MatrixBridgedRoom.getMatrixId(roomId); - - // Get the room - const room = await dataInterface.room(roomId); - - if (!matrixRoomId) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote room...`); - - // Create the missing room - const { mri } = await matrixClient.room.create({ _id: inviterId } as IUser, room, invitedId); - - matrixRoomId = mri; - - console.log(`[${inviterId}-${invitedId}-${roomId}] Remote room created as ${matrixRoomId}...`); - } - - // If the invited user is not remote, let's ensure it exists remotely - if (!invitedUserIsRemote) { - console.log(`[${inviterId}-${invitedId}-${roomId}] Creating remote invited user...`); - - // Check if we already have a matrix id for that user - const existingMatrixId = MatrixBridgedUser.getMatrixId(invitedUser._id); - - if (!existingMatrixId) { - const { mui } = await matrixClient.user.createRemote(invitedUser); - - invitedUserMatrixId = mui; - } else { - invitedUserMatrixId = existingMatrixId; - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Invited user created as ${invitedUserMatrixId}...`); - } - - console.log(`[${inviterId}-${invitedId}-${roomId}] Inviting the user to the room...`); - // Invite && Auto-join if the user is Rocket.Chat controlled - if (!invitedUserIsRemote) { - // Invite the user to the room - await matrixBridge.getInstance().getIntent(bridgedInviterUser.mui).invite(matrixRoomId, invitedUserMatrixId); - - console.log(`[${inviterId}-${invitedId}-${roomId}] Auto-join room...`); - - await matrixBridge.getInstance().getIntent(invitedUserMatrixId).join(matrixRoomId); - } else if (room.t !== 'd') { - // Invite the user to the room but don't wait as this is dependent on the user accepting the invite because we don't control this user - matrixBridge - .getInstance() - .getIntent(bridgedInviterUser.mui) - .invite(matrixRoomId, invitedUserMatrixId) - .catch(() => { - api.broadcast('notify.ephemeralMessage', inviterId, roomId, { - msg: TAPi18n.__('Federation_Matrix_only_owners_can_invite_users', { - postProcess: 'sprintf', - lng: settings.get('Language') || 'en', - }), - }); - }); - } - - // Add the matrix user to the invited room - addUserToRoom(roomId, invitedUser, inviterUser, false); -}; - -export const createRemote = async (u: IUser): Promise => { - const matrixUserId = `@${u.username?.toLowerCase()}:${settings.get('Federation_Matrix_homeserver_domain')}`; - - console.log(`Creating remote user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - await intent.ensureProfile(u.name); - - await intent.setDisplayName(`${u.username} (${u.name})`); - - const payload = { uid: u._id, mui: matrixUserId, remote: true }; - - MatrixBridgedUser.upsert({ uid: u._id }, payload); - - return payload; -}; - -const createLocalUserIfNotExists = async (userId = '', profileInfo: MatrixProfileInfo = {}): Promise => { - const existingUser = await Users.findOneByUsername(formatUserIdAsRCUsername(userId)); - - if (existingUser) { - return existingUser._id; - } - - return Users.create({ - username: removeUselessCharsFromMatrixId(userId), - type: 'user', - status: 'online', - active: true, - roles: ['user'], - name: profileInfo.displayname, - requirePasswordChange: false, - }); -}; - -export const createLocal = async (matrixUserId: string): Promise => { - console.log(`Creating local user ${matrixUserId}...`); - - const intent = matrixBridge.getInstance().getIntent(matrixUserId); - - let currentProfile: MatrixProfileInfo = {}; - - try { - currentProfile = await intent.getProfileInfo(matrixUserId); - } catch (err) { - // no-op - } - - const uid = await createLocalUserIfNotExists(matrixUserId, currentProfile); - const payload = { uid, mui: matrixUserId, remote: false }; - - MatrixBridgedUser.upsert({ uid }, payload); - - return payload; -}; diff --git a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts b/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts deleted file mode 100644 index 7db759e1cc38..000000000000 --- a/apps/meteor/app/federation-v2/server/methods/checkBridgedRoomExists.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MatrixBridgedRoom } from '../../../models/server'; - -export const checkBridgedRoomExists = async (matrixRoomId: string): Promise => { - const existingRoomId = MatrixBridgedRoom.getId(matrixRoomId); - - return !!existingRoomId; -}; diff --git a/apps/meteor/app/federation-v2/server/queue.ts b/apps/meteor/app/federation-v2/server/queue.ts deleted file mode 100644 index f1f0ea02061e..000000000000 --- a/apps/meteor/app/federation-v2/server/queue.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Create the queue -import { queueAsPromised } from 'fastq'; -import * as fastq from 'fastq'; - -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; -import { eventHandler } from './eventHandler'; - -export const matrixEventQueue: queueAsPromised> = fastq.promise(eventHandler, 1); - -export const addToQueue = (event: IMatrixEvent): void => { - console.log(`Queueing ${event.type}...`); - - // TODO: Handle error - matrixEventQueue.push(event).catch((err) => console.error(err)); -}; diff --git a/apps/meteor/app/federation-v2/server/settings.ts b/apps/meteor/app/federation-v2/server/settings.ts deleted file mode 100644 index f10264a3ea37..000000000000 --- a/apps/meteor/app/federation-v2/server/settings.ts +++ /dev/null @@ -1,136 +0,0 @@ -import yaml from 'js-yaml'; -import { SHA256 } from 'meteor/sha'; - -import { getRegistrationInfo } from './config'; -import { Settings } from '../../models/server/raw'; -import { settings, settingsRegistry } from '../../settings/server'; - -settingsRegistry.addGroup('Federation', function () { - this.section('Matrix Bridge', async function () { - this.add('Federation_Matrix_enabled', false, { - readonly: false, - type: 'boolean', - i18nLabel: 'Federation_Matrix_enabled', - i18nDescription: 'Federation_Matrix_enabled_desc', - alert: 'Federation_Matrix_Enabled_Alert', - }); - - const uniqueId = await settings.get('uniqueID'); - const hsToken = SHA256(`hs_${uniqueId}`); - const asToken = SHA256(`as_${uniqueId}`); - - this.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_id', - i18nDescription: 'Federation_Matrix_id_desc', - }); - - this.add('Federation_Matrix_hs_token', hsToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_hs_token', - i18nDescription: 'Federation_Matrix_hs_token_desc', - }); - - this.add('Federation_Matrix_as_token', asToken, { - readonly: true, - type: 'string', - i18nLabel: 'Federation_Matrix_as_token', - i18nDescription: 'Federation_Matrix_as_token_desc', - }); - - this.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_url', - i18nDescription: 'Federation_Matrix_homeserver_url_desc', - alert: 'Federation_Matrix_homeserver_url_alert', - }); - - this.add('Federation_Matrix_homeserver_domain', 'local.rocket.chat', { - type: 'string', - i18nLabel: 'Federation_Matrix_homeserver_domain', - i18nDescription: 'Federation_Matrix_homeserver_domain_desc', - alert: 'Federation_Matrix_homeserver_domain_alert', - }); - - this.add('Federation_Matrix_bridge_url', 'http://host.docker.internal:3300', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_url', - i18nDescription: 'Federation_Matrix_bridge_url_desc', - }); - - this.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { - type: 'string', - i18nLabel: 'Federation_Matrix_bridge_localpart', - i18nDescription: 'Federation_Matrix_bridge_localpart_desc', - }); - - this.add('Federation_Matrix_registration_file', '', { - readonly: true, - type: 'code', - i18nLabel: 'Federation_Matrix_registration_file', - i18nDescription: 'Federation_Matrix_registration_file_desc', - }); - }); -}); - -let registrationFile = {}; - -const updateRegistrationFile = async function (): Promise { - const registrationInfo = getRegistrationInfo(); - - // eslint-disable-next-line @typescript-eslint/camelcase - const { id, hs_token, as_token, sender_localpart } = registrationInfo; - let { url } = registrationInfo; - - if (!url || !url.includes(':')) { - url = `${url}:3300`; - } - - /* eslint-disable @typescript-eslint/camelcase */ - registrationFile = { - id, - hs_token, - as_token, - url, - sender_localpart, - namespaces: { - users: [ - { - exclusive: false, - regex: '.*', - }, - ], - rooms: [ - { - exclusive: false, - regex: '.*', - }, - ], - aliases: [ - { - exclusive: false, - regex: '.*', - }, - ], - }, - }; - /* eslint-enable @typescript-eslint/camelcase */ - - // Update the registration file - await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(registrationFile)); -}; - -settings.watchMultiple( - [ - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - updateRegistrationFile, -); diff --git a/apps/meteor/app/federation-v2/server/startup.ts b/apps/meteor/app/federation-v2/server/startup.ts deleted file mode 100644 index a1495878788c..000000000000 --- a/apps/meteor/app/federation-v2/server/startup.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { settings } from '../../settings/server'; -import { matrixBridge } from './bridge'; -import { bridgeLogger, setupLogger } from './logger'; - -const watchChanges = (): void => { - settings.watchMultiple( - [ - 'Federation_Matrix_enabled', - 'Federation_Matrix_id', - 'Federation_Matrix_hs_token', - 'Federation_Matrix_as_token', - 'Federation_Matrix_homeserver_url', - 'Federation_Matrix_homeserver_domain', - 'Federation_Matrix_bridge_url', - 'Federation_Matrix_bridge_localpart', - ], - async ([enabled]) => { - setupLogger.info(`Federation Matrix is ${enabled ? 'enabled' : 'disabled'}`); - if (!enabled) { - await matrixBridge.stop(); - return; - } - await matrixBridge.start(); - }, - ); -}; - -export const startBridge = (): void => { - watchChanges(); - - bridgeLogger.info(`Running Federation V2: - id: ${settings.get('Federation_Matrix_id')} - bridgeUrl: ${settings.get('Federation_Matrix_bridge_url')} - homeserverURL: ${settings.get('Federation_Matrix_homeserver_url')} - homeserverDomain: ${settings.get('Federation_Matrix_homeserver_domain')} - `); -}; diff --git a/apps/meteor/app/lib/client/methods/sendMessage.js b/apps/meteor/app/lib/client/methods/sendMessage.js index 8dfafeeb5f94..8d3d9bd9532e 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.js +++ b/apps/meteor/app/lib/client/methods/sendMessage.js @@ -32,9 +32,9 @@ Meteor.methods({ message.unread = true; } - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { + // If the room is federated, send the message to matrix only + const { federated } = Rooms.findOne({ _id: message.rid }, { fields: { federated: 1 } }); + if (federated) { return; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.js b/apps/meteor/app/lib/server/methods/sendMessage.js index 91e223322b36..3cf002ba4e0b 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.js +++ b/apps/meteor/app/lib/server/methods/sendMessage.js @@ -7,13 +7,14 @@ import { hasPermission } from '../../../authorization'; import { metrics } from '../../../metrics'; import { settings } from '../../../settings'; import { messageProperties } from '../../../ui-utils'; -import { Users, Messages, Rooms } from '../../../models'; +import { Users, Messages } from '../../../models'; import { sendMessage } from '../functions'; import { RateLimiter } from '../lib'; import { canSendMessage } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; -import { matrixClient } from '../../../federation-v2/server/matrix-client'; +import { federationRoomServiceSender } from '../../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; export function executeSendMessage(uid, message) { if (message.tshow && !message.tmid) { @@ -106,10 +107,10 @@ Meteor.methods({ } try { - // If the room is bridged, send the message to matrix only - const { bridged } = Rooms.findOne({ _id: message.rid }, { fields: { bridged: 1 } }); - if (bridged) { - return matrixClient.message.send({ ...message, u: { _id: uid } }); + if (Promise.await(federationRoomServiceSender.isAFederatedRoom(message.rid))) { + return federationRoomServiceSender.sendMessageFromRocketChat( + FederationRoomSenderConverter.toSendExternalMessageDto(uid, message.rid, message), + ); } return executeSendMessage(uid, message); diff --git a/apps/meteor/app/models/server/raw/Rooms.js b/apps/meteor/app/models/server/raw/Rooms.js index 0d86223b42a8..807b3d365aea 100644 --- a/apps/meteor/app/models/server/raw/Rooms.js +++ b/apps/meteor/app/models/server/raw/Rooms.js @@ -461,8 +461,8 @@ export class RoomsRaw extends BaseRaw { ]); } - setAsBridged(roomId) { - return this.updateOne({ _id: roomId }, { $set: { bridged: true } }); + setAsFederated(roomId) { + return this.updateOne({ _id: roomId }, { $set: { federated: true } }); } findByE2E(options) { diff --git a/apps/meteor/app/slashcommands-bridge/server/index.ts b/apps/meteor/app/slashcommands-bridge/server/index.ts index fa1c1e9d2f9f..b9396a596312 100644 --- a/apps/meteor/app/slashcommands-bridge/server/index.ts +++ b/apps/meteor/app/slashcommands-bridge/server/index.ts @@ -2,36 +2,40 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; import { slashCommands } from '../../utils/lib/slashCommand'; -import { matrixClient } from '../../federation-v2/server/matrix-client'; - -slashCommands.add( - 'bridge', - function Bridge(_command, stringParams, item): void { - if (_command !== 'bridge' || !Match.test(stringParams, String)) { - return; - } - - const [command, ...params] = stringParams.split(' '); - - const { rid: roomId } = item; - - switch (command) { - case 'invite': - // Invite a user - // Example: /bridge invite rc_helena:b.rc.allskar.com - const [userId] = params; - - const currentUserId = Meteor.userId(); - - if (currentUserId) { - Promise.await(matrixClient.user.invite(currentUserId, roomId, `@${userId.replace('@', '')}`)); - } - - break; - } - }, - { - description: 'Invites_an_user_to_a_bridged_room', - params: '#command #user', - }, -); +import { federationRoomServiceSender } from '../../federation-v2/server'; +import { FederationRoomSenderConverter } from '../../federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; + +function Bridge(_command: 'bridge', stringParams: string | undefined, item: Record): void { + if (_command !== 'bridge' || !Match.test(stringParams, String)) { + return; + } + + const [command, ...params] = stringParams.split(' '); + + const { rid: roomId } = item; + + switch (command) { + case 'invite': + // Invite a user + // Example: /bridge invite rc_helena:b.rc.allskar.com + const [userId] = params; + + const currentUserId = Meteor.userId(); + + if (currentUserId) { + const invitee = `@${userId.replace('@', '')}`; + Promise.await( + federationRoomServiceSender.inviteUserToAFederatedRoom( + FederationRoomSenderConverter.toRoomInviteUserDto(currentUserId, roomId, invitee), + ), + ); + } + + break; + } +} + +slashCommands.add('bridge', Bridge, { + description: 'Invites_an_user_to_a_bridged_room', + params: '#command #user', +}); diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js index 902441e0b0fa..3dfdf1189230 100644 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ b/apps/meteor/app/ui-sidenav/client/roomList.js @@ -172,7 +172,7 @@ const mergeSubRoom = (subscription) => { departmentId: 1, source: 1, queuedAt: 1, - bridged: 1, + federated: 1, }, }; @@ -214,7 +214,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, } = room; subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; @@ -253,7 +253,7 @@ const mergeSubRoom = (subscription) => { ts, source, queuedAt, - bridged, + federated, }); }; @@ -297,7 +297,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, } = room; Subscriptions.update( @@ -338,7 +338,7 @@ const mergeRoomSub = (room) => { ts, source, queuedAt, - bridged, + federated, ...getLowerCaseNames(room, sub.name, sub.fname), }, }, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 976b71cf6f2d..ddbe4132fac6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -121,6 +121,7 @@ "@types/rewire": "^2.5.28", "@types/semver": "^7.3.9", "@types/sharp": "^0.30.2", + "@types/sinon": "^10.0.11", "@types/string-strip-html": "^5.0.0", "@types/supertest": "^2.0.11", "@types/toastr": "^2.1.39", @@ -165,6 +166,7 @@ "postcss-url": "^10.1.3", "prettier": "2.6.2", "rewire": "^6.0.0", + "sinon": "^14.0.0", "source-map": "^0.7.3", "stylelint": "^13.13.1", "stylelint-order": "^4.1.0", diff --git a/apps/meteor/server/modules/watchers/publishFields.ts b/apps/meteor/server/modules/watchers/publishFields.ts index bede67ffd03f..6a0ff581ef50 100644 --- a/apps/meteor/server/modules/watchers/publishFields.ts +++ b/apps/meteor/server/modules/watchers/publishFields.ts @@ -105,7 +105,7 @@ export const roomFields = { queuedAt: 1, // Federation fields - bridged: 1, + federated: 1, // fields used by DMs usernames: 1, diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index 045c08a18f4e..f293f36a4640 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -89,4 +89,5 @@ import './v262'; import './v263'; import './v264'; import './v265'; +import './v266'; import './xrun'; diff --git a/apps/meteor/server/startup/migrations/v266.ts b/apps/meteor/server/startup/migrations/v266.ts new file mode 100644 index 000000000000..a2c016b1c571 --- /dev/null +++ b/apps/meteor/server/startup/migrations/v266.ts @@ -0,0 +1,19 @@ +import { addMigration } from '../../lib/migrations'; +import { Rooms } from '../../../app/models/server/raw'; + +addMigration({ + version: 266, + async up() { + await Rooms.updateMany( + { bridged: true }, + { + $set: { + federated: true, + }, + $unset: { + bridged: 1, + }, + }, + ); + }, +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts new file mode 100644 index 000000000000..11050f2681fb --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceReceiver.spec.ts @@ -0,0 +1,436 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import '../../../lib/server.mocks'; + +import { FederationRoomServiceReceiver } from '../../../../../../app/federation-v2/server/application/RoomServiceReceiver'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; +import { EVENT_ORIGIN } from '../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Application - FederationRoomServiceReceiver', () => { + let service: FederationRoomServiceReceiver; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + updateRoomType: sinon.stub(), + updateRoomName: sinon.stub(), + updateRoomTopic: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + }; + const messageAdapter = { + sendMessage: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + joinRoom: sinon.stub(), + }; + + beforeEach(() => { + service = new FederationRoomServiceReceiver( + roomAdapter as any, + userAdapter as any, + messageAdapter as any, + settingsAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.removeUserFromRoom.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.updateRoomType.reset(); + roomAdapter.updateRoomName.reset(); + roomAdapter.updateRoomTopic.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.createFederatedUser.reset(); + messageAdapter.sendMessage.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.joinRoom.reset(); + }); + + describe('#createRoom()', () => { + it('should NOT create users nor room if the room already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create users nor room if the room was created internally and programatically', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.createRoom({ wasInternallyProgramaticallyCreated: true } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the creator if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.createRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the creator if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ externalInviterId: 'externalInviterId', normalizedInviterId: 'normalizedInviterId' } as any); + + expect(userAdapter.createFederatedUser.calledWith(creator)).to.be.true; + }); + + it('should create the room if it does not exists yet', async () => { + const creator = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(creator); + await service.createRoom({ + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + externalRoomName: 'externalRoomName', + } as any); + + const room = FederatedRoom.createInstance( + 'externalRoomId', + 'normalizedRoomId', + creator as FederatedUser, + RoomType.CHANNEL, + 'externalRoomName', + ); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomMembership()', () => { + it('should throw an error if the room does not exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + try { + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room with external room id: externalRoomId'); + } + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to LOCAL', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT throw an error if the room already exists AND event origin is equal to REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.REMOTE } as any); + + expect(bridge.isUserIdFromTheSameHomeserver.called).to.be.true; + }); + + it('should NOT create the inviter if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves({} as any); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter if it does not exists', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(0).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviterId: 'externalInviterId', + normalizedInviterId: 'normalizedInviterId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee if it already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + await service.changeRoomMembership({ externalRoomId: 'externalRoomId', eventOrigin: EVENT_ORIGIN.LOCAL } as any); + + expect(userAdapter.createFederatedUser.calledOnce).to.be.true; + }); + + it('should create the invitee if it does not exists', async () => { + const invitee = FederatedUser.createInstance('externalInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves({} as any); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should create the room if it does not exists yet AND the event origin is REMOTE', async () => { + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const invitee = inviter; + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + bridge.isUserIdFromTheSameHomeserver.onCall(1).resolves(false); + userAdapter.getFederatedUserByExternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(1).resolves(undefined); + userAdapter.getFederatedUserByExternalId.onCall(2).resolves(inviter); + userAdapter.getFederatedUserByExternalId.onCall(3).resolves(invitee); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', inviter as FederatedUser, RoomType.CHANNEL); + expect(roomAdapter.createFederatedRoom.calledWith(room)).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.REMOTE, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should NOT create the room if it already exists yet AND the event origin is REMOTE', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should remove the user from room if its a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: true, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.true; + expect(roomAdapter.addUserToRoom.called).to.be.false; + }); + + it('should add the user from room if its NOT a LEAVE event', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + await service.changeRoomMembership({ + externalRoomId: 'externalRoomId', + normalizedRoomId: 'normalizedRoomId', + eventOrigin: EVENT_ORIGIN.LOCAL, + roomType: RoomType.CHANNEL, + externalInviteeId: 'externalInviteeId', + leave: false, + normalizedInviteeId: 'normalizedInviteeId', + } as any); + + expect(roomAdapter.removeUserFromRoom.called).to.be.false; + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#receiveExternalMessage()', () => { + it('should NOT send a message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should NOT send a message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.called).to.be.false; + }); + + it('should send a message if the room and the sender already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves({} as any); + await service.receiveExternalMessage({ + text: 'text', + } as any); + + expect(messageAdapter.sendMessage.calledWith({}, 'text', {})).to.be.true; + }); + }); + + describe('#changeJoinRules()', () => { + it('should NOT change the room type if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should NOT change the room type if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + + expect(roomAdapter.updateRoomType.called).to.be.false; + }); + + it('should change the room type if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeJoinRules({ + roomType: RoomType.CHANNEL, + } as any); + room.internalReference.t = RoomType.CHANNEL; + expect(roomAdapter.updateRoomType.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomName()', () => { + it('should NOT change the room name if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should NOT change the room name if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + normalizedRoomName: 'normalizedRoomName', + } as any); + + expect(roomAdapter.updateRoomName.called).to.be.false; + }); + + it('should change the room name if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomName({ + roomnormalizedRoomNameType: 'normalizedRoomName', + } as any); + room.internalReference.name = 'normalizedRoomName'; + room.internalReference.fname = 'normalizedRoomName'; + expect(roomAdapter.updateRoomName.calledWith(room)).to.be.true; + }); + }); + + describe('#changeRoomTopic()', () => { + it('should NOT change the room topic if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should NOT change the room topic if it exists and is a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + + expect(roomAdapter.updateRoomTopic.called).to.be.false; + }); + + it('should change the room topic if it exists and is NOT a direct message', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.t = RoomType.PRIVATE_GROUP; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + await service.changeRoomTopic({ + roomTopic: 'roomTopic', + } as any); + room.internalReference.description = 'roomTopic'; + expect(roomAdapter.updateRoomTopic.calledWith(room)).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts new file mode 100644 index 000000000000..8fa7719fb2ad --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/application/RoomServiceSender.spec.ts @@ -0,0 +1,311 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { FederationRoomServiceSender } from '../../../../../../app/federation-v2/server/application/RoomServiceSender'; +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Application - FederationRoomServiceSender', () => { + let service: FederationRoomServiceSender; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + getFederatedRoomByInternalId: sinon.stub(), + createFederatedRoom: sinon.stub(), + updateFederatedRoomByInternalRoomId: sinon.stub(), + removeUserFromRoom: sinon.stub(), + addUserToRoom: sinon.stub(), + getInternalRoomById: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + getFederatedUserByInternalId: sinon.stub(), + createFederatedUser: sinon.stub(), + getInternalUserById: sinon.stub(), + getFederatedUserByInternalUsername: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub(), + }; + const bridge = { + getUserProfileInformation: sinon.stub().resolves({}), + isUserIdFromTheSameHomeserver: sinon.stub(), + sendMessage: sinon.stub(), + createUser: sinon.stub(), + inviteToRoom: sinon.stub().returns(new Promise((resolve) => resolve({}))), + createRoom: sinon.stub(), + joinRoom: sinon.stub(), + }; + const notificationAdapter = {}; + const room = FederatedRoom.build(); + const user = FederatedRoom.build(); + + beforeEach(() => { + service = new FederationRoomServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + notificationAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + roomAdapter.getFederatedRoomByInternalId.reset(); + roomAdapter.createFederatedRoom.reset(); + roomAdapter.updateFederatedRoomByInternalRoomId.reset(); + roomAdapter.addUserToRoom.reset(); + roomAdapter.getInternalRoomById.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + userAdapter.getFederatedUserByInternalId.reset(); + userAdapter.getInternalUserById.reset(); + userAdapter.createFederatedUser.reset(); + userAdapter.getFederatedUserByInternalUsername.reset(); + settingsAdapter.getHomeServerDomain.reset(); + bridge.isUserIdFromTheSameHomeserver.reset(); + bridge.sendMessage.reset(); + bridge.createUser.reset(); + bridge.createRoom.reset(); + bridge.inviteToRoom.reset(); + bridge.joinRoom.reset(); + }); + + describe('#inviteUserToAFederatedRoom()', () => { + it('should NOT create the inviter user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({} as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the inviter user both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalUsername.resolves(user); + userAdapter.getFederatedUserByInternalId.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalId.onCall(1).resolves(user); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + bridge.createUser.resolves('externalInviterId'); + await service.inviteUserToAFederatedRoom({ externalInviterId: 'externalInviterId' } as any); + const inviter = FederatedUser.createInstance('externalInviterId', { + name: 'name', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(bridge.createUser.calledWith('username', 'name', 'domain')).to.be.true; + expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; + }); + + it('should NOT create the invitee user if the user already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(userAdapter.createFederatedUser.called).to.be.false; + }); + + it('should create the invitee user internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.onCall(0).resolves(undefined); + userAdapter.getFederatedUserByInternalUsername.onCall(1).resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const invitee = FederatedUser.createInstance('rawInviteeId', { + name: 'normalizedInviteeId', + username: 'normalizedInviteeId', + existsOnlyOnProxyServer: false, + }); + + expect(userAdapter.createFederatedUser.calledWith(invitee)).to.be.true; + }); + + it('should NOT create the room if it already exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + userAdapter.getFederatedUserByInternalUsername.resolves({} as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + + expect(roomAdapter.createFederatedRoom.called).to.be.false; + }); + + it('should create the room both externally and internally if it does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ externalId: 'externalInviteeId' } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.createUser.resolves('externalInviterId'); + bridge.createRoom.resolves('externalRoomId'); + roomAdapter.getFederatedRoomByInternalId.onCall(0).resolves(undefined); + roomAdapter.getFederatedRoomByInternalId.onCall(1).resolves(room); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ normalizedInviteeId: 'normalizedInviteeId', rawInviteeId: 'rawInviteeId' } as any); + const roomResult = FederatedRoom.createInstance('externalRoomId', 'externalRoomId', user as any, RoomType.CHANNEL, 'roomName'); + + expect(bridge.createRoom.calledWith('externalInviterId', 'externalInviteeId', RoomType.CHANNEL, 'roomName', 'topic')).to.be.true; + expect(roomAdapter.updateFederatedRoomByInternalRoomId.calledWith('internalRoomId', roomResult)).to.be.true; + }); + + it('should create, invite and join the user to the room in the proxy home server if the invitee is from the same homeserver', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ _id: 'internalRoomId', t: RoomType.CHANNEL, name: 'roomName', topic: 'topic' } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + settingsAdapter.getHomeServerDomain.returns('domain'); + bridge.isUserIdFromTheSameHomeserver.resolves(true); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.createUser.calledWith('inviteeUsernameOnly', 'usernameInvitee', 'domain')).to.be.true; + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + expect(bridge.joinRoom.calledWith('externalRoomId', 'externalInviteeId')).to.be.true; + }); + + it('should invite the user to an external room if the room is NOT direct message(on DMs, they are invited during the creational process)', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + roomAdapter.getInternalRoomById.resolves({ + _id: 'internalRoomId', + t: RoomType.DIRECT_MESSAGE, + name: 'roomName', + topic: 'topic', + } as any); + userAdapter.getInternalUserById.resolves({ username: 'username', name: 'name' } as any); + room.externalId = 'externalRoomId'; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.calledWith('externalRoomId', 'externalInviterId', 'externalInviteeId')).to.be.true; + }); + + it('should NOT invite any user externally if the user is not from the same home server AND it was already invited when creating the room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(bridge.inviteToRoom.called).to.be.false; + expect(bridge.createUser.called).to.be.false; + expect(bridge.joinRoom.called).to.be.false; + }); + + it('should always add the user to the internal room', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalInviterId' } as any); + userAdapter.getFederatedUserByInternalUsername.resolves({ + externalId: 'externalInviteeId', + internalReference: { name: 'usernameInvitee' }, + } as any); + room.internalReference = {} as any; + room.internalReference.t = RoomType.DIRECT_MESSAGE; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + bridge.isUserIdFromTheSameHomeserver.resolves(false); + bridge.inviteToRoom.returns(new Promise((resolve) => resolve({}))); + await service.inviteUserToAFederatedRoom({ + normalizedInviteeId: 'normalizedInviteeId', + rawInviteeId: 'rawInviteeId', + inviteeUsernameOnly: 'inviteeUsernameOnly', + } as any); + + expect(roomAdapter.addUserToRoom.called).to.be.true; + }); + }); + + describe('#sendMessageFromRocketChat()', () => { + it('should throw an error if the sender does not exists ', async () => { + userAdapter.getFederatedUserByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalSenderId: 'internalSenderId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find user id for internalSenderId'); + } + }); + + it('should throw an error if the room does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves({} as any); + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + try { + await service.sendMessageFromRocketChat({ internalRoomId: 'internalRoomId' } as any); + } catch (e: any) { + expect(e.message).to.be.equal('Could not find room id for internalRoomId'); + } + }); + + it('should send the message through the bridge', async () => { + userAdapter.getFederatedUserByInternalId.resolves({ externalId: 'externalId' } as any); + roomAdapter.getFederatedRoomByInternalId.resolves({ externalId: 'externalId' } as any); + await service.sendMessageFromRocketChat({ message: { msg: 'text' } } as any); + expect(bridge.sendMessage.calledWith('externalId', 'externalId', 'text')).to.be.true; + }); + }); + + describe('#isAFederatedRoom()', () => { + it('should return false if internalRoomId is undefined', async () => { + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return false if the room does not exist', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + expect(await service.isAFederatedRoom('')).to.be.false; + }); + + it('should return true if the room is NOT federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = false; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.false; + }); + + it('should return true if the room is federated', async () => { + const room = FederatedRoom.build(); + room.internalReference = {} as any; + room.internalReference.federated = true; + roomAdapter.getFederatedRoomByInternalId.resolves(room); + expect(await service.isAFederatedRoom('internalRoomId')).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts new file mode 100644 index 000000000000..b6d7f0011368 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedRoom.spec.ts @@ -0,0 +1,169 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { expect } from 'chai'; + +import { FederatedRoom } from '../../../../../../app/federation-v2/server/domain/FederatedRoom'; + +describe('Federation - Domain - FederatedRoom', () => { + const members = [{ internalReference: { id: 'userId' } }, { internalReference: { id: 'userId2' } }] as any; + + describe('#createInstance()', () => { + it('should set the internal room name when it was provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any, 'myRoomName'); + expect(federatedRoom.internalReference.name).to.be.equal('myRoomName'); + expect(federatedRoom.internalReference.fname).to.be.equal('myRoomName'); + }); + + it('should generate automatically a room name when it was not provided', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, 'p' as any); + expect(federatedRoom.internalReference.name).to.be.equal('Federation-externalId'); + expect(federatedRoom.internalReference.fname).to.be.equal('Federation-externalId'); + }); + + it('should set the members property when the room is a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.members).to.be.eql(members); + }); + + it('should NOT set the members property when the room is NOT a direct message one', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.members).to.be.undefined; + }); + + it('should return an instance of FederatedRoom', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom).to.be.instanceOf(FederatedRoom); + }); + }); + + describe('#isDirectMessage()', () => { + it('should return true if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(federatedRoom.isDirectMessage()).to.be.true; + }); + + it('should return false if its NOT a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.CHANNEL); + expect(federatedRoom.isDirectMessage()).to.be.false; + }); + }); + + describe('#setRoomType()', () => { + it('should set the Room type if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.setRoomType(RoomType.CHANNEL); + expect(federatedRoom.internalReference.t).to.be.equal(RoomType.CHANNEL); + }); + + it('should throw an error when trying to set the room type if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.setRoomType(RoomType.CHANNEL)).to.be.throw('Its not possible to change a direct message type'); + }); + }); + + describe('#changeRoomName()', () => { + it('should change the Room name if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomName('newName'); + expect(federatedRoom.internalReference.name).to.be.equal('newName'); + expect(federatedRoom.internalReference.fname).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room name if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomName('newName')).to.be.throw('Its not possible to change a direct message name'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#changeRoomTopic()', () => { + it('should change the Room topic if its not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.PRIVATE_GROUP); + federatedRoom.changeRoomTopic('newName'); + expect(federatedRoom.internalReference.description).to.be.equal('newName'); + }); + + it('should throw an error when trying to change the room topic if its a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance('!externalId@id', 'externalId', { id: 'userId' } as any, RoomType.DIRECT_MESSAGE); + expect(() => federatedRoom.changeRoomTopic('newName')).to.be.throw('Its not possible to change a direct message topic'); + }); + }); + + describe('#getMembers()', () => { + it('should return the internalReference members if the room is a direct message', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql(members.map((user: any) => user.internalReference)); + }); + + it('should return an empty array if the room is not a direct message room', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.CHANNEL, + '', + members, + ); + expect(federatedRoom.getMembers()).to.be.eql([]); + }); + }); + + describe('#isFederated()', () => { + it('should return true if the room is federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + federatedRoom.internalReference.federated = true; + expect(federatedRoom.isFederated()).to.be.true; + }); + + it('should return false if the room is NOT federated', () => { + const federatedRoom = FederatedRoom.createInstance( + '!externalId@id', + 'externalId', + { id: 'userId' } as any, + RoomType.DIRECT_MESSAGE, + '', + members, + ); + expect(federatedRoom.isFederated()).to.be.false; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts new file mode 100644 index 000000000000..386877bbd858 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/domain/FederatedUser.spec.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; + +import { FederatedUser } from '../../../../../../app/federation-v2/server/domain/FederatedUser'; + +describe('Federation - Domain - FederatedUser', () => { + describe('#createInstance()', () => { + it('should set the internal user name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.username).to.be.equal('username'); + }); + + it('should set the internal name when it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: 'name', + username: '', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser.internalReference.name).to.be.equal('name'); + }); + + it('should set the existsOnlyOnProxyServer it was provided', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: true, + }); + expect(federatedUser.existsOnlyOnProxyServer).to.be.true; + }); + + it('should return an instance of FederatedUser', () => { + const federatedUser = FederatedUser.createInstance('@marcos:matrix.org', { + name: '', + username: 'username', + existsOnlyOnProxyServer: false, + }); + expect(federatedUser).to.be.instanceOf(FederatedUser); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts new file mode 100644 index 000000000000..31a67945fa15 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts @@ -0,0 +1,328 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { expect } from 'chai'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { MatrixRoomReceiverConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver'; +import { + FederationRoomCreateInputDto, + FederationRoomChangeMembershipDto, + FederationRoomSendInternalMessageDto, + FederationRoomChangeJoinRulesDto, + FederationRoomChangeNameDto, + FederationRoomChangeTopicDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; +import { MatrixEventType } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType'; +import { RoomJoinRules } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { AddMemberToRoomMembership } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; +import { EVENT_ORIGIN } from '../../../../../../../../app/federation-v2/server/domain/IFederationBridge'; + +describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', () => { + describe('#toRoomCreateDto()', () => { + const event = { + content: { was_internally_programatically_created: true, name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomCreateInputDto', () => { + expect(MatrixRoomReceiverConverter.toRoomCreateDto({} as any)).to.be.instanceOf(FederationRoomCreateInputDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should set wasInternallyProgramaticallyCreated accordingly to the event', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto({ content: event.content } as any); + expect(result.wasInternallyProgramaticallyCreated).to.be.true; + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomCreateDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + wasInternallyProgramaticallyCreated: true, + }); + }); + }); + + describe('#toChangeRoomMembershipDto()', () => { + const event = { + content: { name: 'roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + state_key: '@marcos.defendi2:matrix.org', + }; + + it('should return an instance of FederationRoomChangeMembershipDto', () => { + expect(MatrixRoomReceiverConverter.toChangeRoomMembershipDto({} as any)).to.be.instanceOf(FederationRoomChangeMembershipDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should return the external room name and room type when the room state is present on the event and it has the correct events', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const state = [ + { type: MatrixEventType.ROOM_NAME_CHANGED, content: { name: event.content.name } }, + { type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }, + ]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ unsigned: { invite_room_state: state } } as any); + expect(result.externalRoomName).to.be.equal(event.content.name); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.JOIN } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ invite_room_state: state } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert to the expected (direct) room type when the join rule is equal to INVITE and its a direct message', () => { + const state = [{ type: MatrixEventType.ROOM_JOIN_RULES_CHANGED, content: { join_rule: RoomJoinRules.INVITE } }]; + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + invite_room_state: state, + content: { is_direct: true }, + } as any); + expect(result.roomType).to.be.equal(RoomType.DIRECT_MESSAGE); + }); + + it('should convert the inviter id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.normalizedInviterId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the invitee id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.normalizedInviteeId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the inviter id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: event.sender } as any); + expect(result.inviterUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should convert the invitee id to the a rc-format username like (without any @ in it and just the part before the ":")', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ state_key: event.sender } as any); + expect(result.inviteeUsernameOnly).to.be.equal('marcos.defendi'); + }); + + it('should set leave to true if its a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.LEAVE }, + } as any); + expect(result.leave).to.be.true; + }); + + it('should set leave to false if its NOT a LEAVE event', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ + content: { membership: AddMemberToRoomMembership.JOIN }, + } as any); + expect(result.leave).to.be.false; + }); + + it('should set the event origin as REMOTE if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix2.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.REMOTE); + }); + + it('should set the event origin as LOCAL if the users are from different home servers', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto({ sender: 'a:matrix.org', state_key: 'a:matrix.org' } as any); + expect(result.eventOrigin).to.be.equal(EVENT_ORIGIN.LOCAL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalInviterId: '@marcos.defendi:matrix.org', + normalizedInviterId: 'marcos.defendi:matrix.org', + externalInviteeId: '@marcos.defendi2:matrix.org', + normalizedInviteeId: 'marcos.defendi2:matrix.org', + inviteeUsernameOnly: 'marcos.defendi2', + inviterUsernameOnly: 'marcos.defendi', + eventOrigin: EVENT_ORIGIN.LOCAL, + leave: false, + }); + }); + }); + + describe('#toSendRoomMessageDto()', () => { + const event = { + content: { body: 'msg' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomSendInternalMessageDto', () => { + expect(MatrixRoomReceiverConverter.toSendRoomMessageDto({} as any)).to.be.instanceOf(FederationRoomSendInternalMessageDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the sender id to the a rc-format like (without any @ in it)', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ sender: event.sender } as any); + expect(result.normalizedSenderId).to.be.equal('marcos.defendi:matrix.org'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalSenderId: '@marcos.defendi:matrix.org', + normalizedSenderId: 'marcos.defendi:matrix.org', + text: 'msg', + }); + }); + }); + + describe('#toRoomChangeJoinRulesDto()', () => { + const event = { + content: { join_rule: RoomJoinRules.JOIN }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeJoinRulesDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({} as any)).to.be.instanceOf(FederationRoomChangeJoinRulesDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert to the expected (private) room type when the join rule is equal to INVITE', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.INVITE } } as any); + expect(result.roomType).to.be.equal(RoomType.PRIVATE_GROUP); + }); + + it('should convert to the expected (channel) room type when the join rule is equal to JOIN', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto({ content: { join_rule: RoomJoinRules.JOIN } } as any); + expect(result.roomType).to.be.equal(RoomType.CHANNEL); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomType: RoomType.CHANNEL, + }); + }); + }); + + describe('#toRoomChangeNameDto()', () => { + const event = { + content: { name: '@roomName' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of toRoomChangeNameDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeNameDto({} as any)).to.be.instanceOf(FederationRoomChangeNameDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the roomName to a normalized version without starting with @', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto({ content: event.content } as any); + expect(result.normalizedRoomName).to.be.equal('roomName'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeNameDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + normalizedRoomName: 'roomName', + }); + }); + }); + + describe('#toRoomChangeTopicDto()', () => { + const event = { + content: { topic: 'room topic' }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomChangeTopicDto', () => { + expect(MatrixRoomReceiverConverter.toRoomChangeTopicDto({} as any)).to.be.instanceOf(FederationRoomChangeTopicDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto(event as any); + expect(result).to.be.eql({ + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + roomTopic: 'room topic', + }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts new file mode 100644 index 000000000000..a8ff82b762f2 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/BaseEvent.spec.ts @@ -0,0 +1,45 @@ +import { expect, spy } from 'chai'; + +import { MatrixBaseEventHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers/BaseEvent'; + +describe('Federation - Infrastructure - Matrix - MatrixBaseEventHandler', () => { + describe('#equals()', () => { + class MyHandler extends MatrixBaseEventHandler { + public constructor(type: any) { + super(type); + } + + public handle(): Promise { + throw new Error('Method not implemented.'); + } + } + const myHandler = new MyHandler('type' as any); + + it('should return true if the type is equals to the provided one', () => { + expect(myHandler.equals('type' as any)).to.be.true; + }); + + it('should return false if the type is different to the provided one', () => { + expect(myHandler.equals('different' as any)).to.be.false; + }); + }); + + describe('#handle()', () => { + const spyFn = spy(); + class MyHandler extends MatrixBaseEventHandler { + public constructor(type: any) { + super(type); + } + + public async handle(): Promise { + spyFn(); + } + } + const myHandler = new MyHandler('type' as any); + + it('should call the handler fn in the implementated class', () => { + myHandler.handle(); + expect(spyFn).to.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts new file mode 100644 index 000000000000..bbdde4978286 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/matrix/handlers/MatrixEventsHandler.spec.ts @@ -0,0 +1,25 @@ +import { expect, spy } from 'chai'; + +import { MatrixEventsHandler } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers'; + +describe('Federation - Infrastructure - Matrix - MatrixEventsHandler', () => { + describe('#handleEvent()', () => { + const spyFn = spy(); + const myHandler = new MatrixEventsHandler([ + { + equals: (eventType: string): boolean => eventType === 'eventType', + handle: spyFn, + }, + ] as any); + + it('should call the handler fn properly', async () => { + await myHandler.handleEvent({ type: 'eventType' } as any); + expect(spyFn).to.have.been.called.with({ type: 'eventType' }); + }); + + it('should NOT call the handler if there is no handler for the event', async () => { + await myHandler.handleEvent({ type: 'eventType2' } as any); + expect(spyFn).to.not.be.called; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts new file mode 100644 index 000000000000..a79437b3494d --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/queue/InMemoryQueue.spec.ts @@ -0,0 +1,28 @@ +import { expect, spy } from 'chai'; +import mock from 'mock-require'; + +import { InMemoryQueue } from '../../../../../../../app/federation-v2/server/infrastructure/queue/InMemoryQueue'; + +mock('fastq', { + promise: (handler: Function) => ({ + push: async (task: any): Promise => handler(task), + }), +}); + +describe('Federation - Infrastructure - Queue - InMemoryQueue', () => { + const queue = new InMemoryQueue(); + + describe('#addToQueue()', () => { + it('should throw an error if the instance was not set beforehand', () => { + expect(() => queue.addToQueue({})).to.throw('You need to set the handler first'); + }); + + it('should push the task to the queue instance to be handled when the instance was properly defined', () => { + const spiedCb = spy(); + const concurrency = 1; + queue.setHandler(spiedCb, concurrency); + queue.addToQueue({ task: 'my-task' }); + expect(spiedCb).to.have.been.called.with({ task: 'my-task' }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts new file mode 100644 index 000000000000..19edac2efe00 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Room.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatRoomAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts new file mode 100644 index 000000000000..f5d7859a0c1c --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/Settings.spec.ts @@ -0,0 +1,3 @@ +// describe('Federation - Infrastructure - RocketChat - RocketChatSettingsAdapter', () => { + +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts new file mode 100644 index 000000000000..29bf5f30b0aa --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/adapters/User.spec.ts @@ -0,0 +1,5 @@ +// // import { expect } from 'chai'; + +// describe('Federation - Infrastructure - RocketChat - RocketChatUserAdapter', () => { +// +// }); diff --git a/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts new file mode 100644 index 000000000000..575f51ec3a92 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/unit/infrastructure/rocket-chat/converters/RoomSender.spec.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { IMessage } from '@rocket.chat/core-typings'; + +import { FederationRoomSenderConverter } from '../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender'; +import { + FederationRoomInviteUserDto, + FederationRoomSendExternalMessageDto, +} from '../../../../../../../../app/federation-v2/server/application/input/RoomSenderDto'; + +describe('Federation - Infrastructure - RocketChat - FederationRoomSenderConverter', () => { + describe('#toRoomInviteUserDto()', () => { + it('should return an instance of FederationRoomInviteUserDto', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'), + ).to.be.instanceOf(FederationRoomInviteUserDto); + }); + + it('should return the normalizedInviteeId property without any @ if any', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .normalizedInviteeId, + ).to.be.equal('externalInviteeId:server-name.com'); + }); + + it('should return the inviteeUsernameOnly property without any @ if any and only the first part before ":"', () => { + expect( + FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', '@externalInviteeId:server-name.com') + .inviteeUsernameOnly, + ).to.be.equal('externalInviteeId'); + }); + + it('should return the normalizedInviteeId AND inviteeUsernameOnly equals to the rawInviteeId if it does not have any special chars', () => { + const result = FederationRoomSenderConverter.toRoomInviteUserDto('internalInviterId', 'internalRoomId', 'externalInviteeId'); + expect(result.rawInviteeId).to.be.equal('externalInviteeId'); + expect(result.normalizedInviteeId).to.be.equal('externalInviteeId'); + expect(result.inviteeUsernameOnly).to.be.equal('externalInviteeId'); + }); + + it('should have all the properties set', () => { + const internalInviterId = 'internalInviterId'; + const internalRoomId = 'internalRoomId'; + const externalInviteeId = 'externalInviteeId'; + const result: any = FederationRoomSenderConverter.toRoomInviteUserDto(internalInviterId, internalRoomId, externalInviteeId); + expect(result).to.be.eql({ + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId: externalInviteeId, + inviteeUsernameOnly: externalInviteeId, + }); + }); + }); + describe('#toSendExternalMessageDto()', () => { + it('should return an instance of FederationRoomSendExternalMessageDto', () => { + expect( + FederationRoomSenderConverter.toSendExternalMessageDto('internalSenderId', 'internalRoomId', { msg: 'text' } as IMessage), + ).to.be.instanceOf(FederationRoomSendExternalMessageDto); + }); + + it('should have all the properties set', () => { + const internalSenderId = 'internalSenderId'; + const internalRoomId = 'internalRoomId'; + const msg = { msg: 'text' } as IMessage; + const result: any = FederationRoomSenderConverter.toSendExternalMessageDto(internalSenderId, internalRoomId, msg); + expect(result).to.be.eql({ + internalSenderId, + internalRoomId, + message: msg, + }); + }); + }); +}); diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index dc09fe72a312..647fbe290171 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -84,6 +84,7 @@ export interface IRoom extends IRocketChatRecord { description?: string; createdOTR?: boolean; e2eKeyId?: string; + federated?: boolean; channel?: { _id: string }; } diff --git a/yarn.lock b/yarn.lock index b8f3140d3d2a..b6f5c8dcf825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4826,6 +4826,7 @@ __metadata: "@types/rewire": ^2.5.28 "@types/semver": ^7.3.9 "@types/sharp": ^0.30.2 + "@types/sinon": ^10.0.11 "@types/speakeasy": ^2.0.7 "@types/string-strip-html": ^5.0.0 "@types/supertest": ^2.0.11 @@ -4984,6 +4985,7 @@ __metadata: rewire: ^6.0.0 semver: ^7.3.7 sharp: ^0.30.4 + sinon: ^14.0.0 sip.js: ^0.20.0 sodium-native: ^3.3.0 sodium-plus: ^0.9.0 @@ -5248,7 +5250,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^1.7.0": +"@sinonjs/commons@npm:^1.6.0, @sinonjs/commons@npm:^1.7.0, @sinonjs/commons@npm:^1.8.3": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" dependencies: @@ -5257,6 +5259,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:>=5, @sinonjs/fake-timers@npm:^9.1.2": + version: 9.1.2 + resolution: "@sinonjs/fake-timers@npm:9.1.2" + dependencies: + "@sinonjs/commons": ^1.7.0 + checksum: 7d3aef54e17c1073101cb64d953157c19d62a40e261a30923fa1ee337b049c5f29cc47b1f0c477880f42b5659848ba9ab897607ac8ea4acd5c30ddcfac57fca6 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^8.0.1": version: 8.1.0 resolution: "@sinonjs/fake-timers@npm:8.1.0" @@ -5266,6 +5277,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^6.1.1": + version: 6.1.1 + resolution: "@sinonjs/samsam@npm:6.1.1" + dependencies: + "@sinonjs/commons": ^1.6.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: a09b0914bf573f0da82bd03c64ba413df81a7c173818dc3f0a90c2652240ac835ef583f4d52f0b215e626633c91a4095c255e0669f6ead97241319f34f05e7fc + languageName: node + linkType: hard + +"@sinonjs/text-encoding@npm:^0.7.1": + version: 0.7.1 + resolution: "@sinonjs/text-encoding@npm:0.7.1" + checksum: 130de0bb568c5f8a611ec21d1a4e3f80ab0c5ec333010f49cfc1adc5cba6d8808699c8a587a46b0f0b016a1f4c1389bc96141e773e8460fcbb441875b2e91ba7 + languageName: node + linkType: hard + "@slack/client@npm:^4.12.0": version: 4.12.0 resolution: "@slack/client@npm:4.12.0" @@ -7807,6 +7836,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^10.0.11": + version: 10.0.11 + resolution: "@types/sinon@npm:10.0.11" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 196f3e26985dca5dfb593592e4b64463e536c047a9f43aa2b328b16024a3b0e3fb27b7a3f3972c6ef75749f55012737eb6c63a1c2e9782b7fe5cbbd25f75fd62 + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd + languageName: node + linkType: hard + "@types/sizzle@npm:*": version: 2.3.3 resolution: "@types/sizzle@npm:2.3.3" @@ -13944,6 +13989,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -21338,6 +21390,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^4.0.2": + version: 4.2.1 + resolution: "just-extend@npm:4.2.1" + checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c + languageName: node + linkType: hard + "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" @@ -24088,6 +24147,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.1": + version: 5.1.1 + resolution: "nise@npm:5.1.1" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ">=5" + "@sinonjs/text-encoding": ^0.7.1 + just-extend: ^4.0.2 + path-to-regexp: ^1.7.0 + checksum: d8be29e84a014743c9a10f428fac86f294ac5f92bed1f606fe9b551e935f494d8e0ce1af8a12673c6014010ec7f771f2d48aa5c8e116f223eb4f40c5e1ab44b3 + languageName: node + linkType: hard + "nkeys.js@npm:^1.0.0-9": version: 1.0.0-9 resolution: "nkeys.js@npm:1.0.0-9" @@ -25750,6 +25822,15 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^1.7.0": + version: 1.8.0 + resolution: "path-to-regexp@npm:1.8.0" + dependencies: + isarray: 0.0.1 + checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd + languageName: node + linkType: hard + "path-to-regexp@npm:^6.2.0": version: 6.2.0 resolution: "path-to-regexp@npm:6.2.0" @@ -30106,6 +30187,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^14.0.0": + version: 14.0.0 + resolution: "sinon@npm:14.0.0" + dependencies: + "@sinonjs/commons": ^1.8.3 + "@sinonjs/fake-timers": ^9.1.2 + "@sinonjs/samsam": ^6.1.1 + diff: ^5.0.0 + nise: ^5.1.1 + supports-color: ^7.2.0 + checksum: b2aeeb0cdc2cd30f904ccbcd60bae4e1b3dcf3aeeface09c1832db0336be0dbaa461f3b91b769bed84f05c83d45d5072a9da7ee14bc7289daeda2a1214fe173c + languageName: node + linkType: hard + "sip.js@npm:^0.20.0": version: 0.20.0 resolution: "sip.js@npm:0.20.0" @@ -31457,7 +31552,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -32666,7 +32761,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5": +"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15