diff --git a/apps/meteor/app/federation-v2/client/Federation.ts b/apps/meteor/app/federation-v2/client/Federation.ts index 3b5f11684616..ebfd37b09bf6 100644 --- a/apps/meteor/app/federation-v2/client/Federation.ts +++ b/apps/meteor/app/federation-v2/client/Federation.ts @@ -2,7 +2,12 @@ import { ValueOf } from '@rocket.chat/core-typings'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; -const allowedActionsInFederatedRooms: ValueOf[] = [RoomMemberActions.REMOVE_USER]; +const allowedActionsInFederatedRooms: ValueOf[] = [ + RoomMemberActions.REMOVE_USER, + RoomMemberActions.INVITE, + RoomMemberActions.JOIN, + RoomMemberActions.LEAVE, +]; export class Federation { public static federationActionAllowed(action: ValueOf): boolean { diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts index de9da171573b..21d4f3dbf840 100644 --- a/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts @@ -79,11 +79,11 @@ export class FederationRoomServiceReceiver { if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.LOCAL) { throw new Error(`Could not find room with external room id: ${externalRoomId}`); } - const isInviterFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + const isInviterFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver( externalInviterId, this.rocketSettingsAdapter.getHomeServerDomain(), ); - const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + const isInviteeFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver( externalInviteeId, this.rocketSettingsAdapter.getHomeServerDomain(), ); @@ -116,7 +116,6 @@ export class FederationRoomServiceReceiver { const federatedInviteeUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviteeId); const federatedInviterUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalInviterId); - if (!affectedFederatedRoom && eventOrigin === EVENT_ORIGIN.REMOTE) { const members = [federatedInviterUser, federatedInviteeUser] as FederatedUser[]; const newFederatedRoom = FederatedRoom.createInstance( @@ -140,6 +139,27 @@ export class FederationRoomServiceReceiver { federatedInviterUser as FederatedUser, ); } + if (affectedFederatedRoom?.isDirectMessage() && eventOrigin === EVENT_ORIGIN.REMOTE) { + const membersUsernames = [ + ...(affectedFederatedRoom.internalReference?.usernames || []), + federatedInviteeUser?.internalReference.username as string, + ]; + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + federatedInviterUser as FederatedUser, + RoomType.DIRECT_MESSAGE, + externalRoomName, + ); + if (affectedFederatedRoom.internalReference?.usernames?.includes(federatedInviteeUser?.internalReference.username || '')) { + return; + } + await this.rocketRoomAdapter.removeDirectMessageRoom(affectedFederatedRoom); + await this.rocketRoomAdapter.createFederatedRoomForDirectMessage(newFederatedRoom, membersUsernames); + await this.bridge.inviteToRoom(externalRoomId, externalInviterId, externalInviteeId); + return; + } + await this.rocketRoomAdapter.addUserToRoom( federatedRoom as FederatedRoom, federatedInviteeUser as FederatedUser, diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts index 54b9b65ff470..1f8ae0c766e7 100644 --- a/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts @@ -1,5 +1,5 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { IMessage, IUser } from '@rocket.chat/core-typings'; +import { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { FederatedRoom } from '../domain/FederatedRoom'; import { FederatedUser } from '../domain/FederatedUser'; @@ -52,14 +52,14 @@ export class FederationRoomServiceSender { } const federatedInviterUser = (await this.rocketUserAdapter.getFederatedUserByInternalId(internalInviterId)) as FederatedUser; const federatedInviteeUser = (await this.rocketUserAdapter.getFederatedUserByInternalUsername(normalizedInviteeId)) as FederatedUser; - const isInviteeFromTheSameHomeServer = await this.bridge.isUserIdFromTheSameHomeserver( + const isInviteeFromTheSameHomeServer = this.bridge.isUserIdFromTheSameHomeserver( rawInviteeId, this.rocketSettingsAdapter.getHomeServerDomain(), ); const internalRoomId = FederatedRoom.buildRoomIdForDirectMessages(federatedInviterUser, federatedInviteeUser); if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) { - const externalRoomId = await this.bridge.createDirectMessageRoom(federatedInviterUser.externalId, federatedInviteeUser.externalId); + const externalRoomId = await this.bridge.createDirectMessageRoom(federatedInviterUser.externalId, [federatedInviteeUser.externalId]); const newFederatedRoom = FederatedRoom.createInstance( externalRoomId, externalRoomId, @@ -68,14 +68,14 @@ export class FederationRoomServiceSender { '', [federatedInviterUser, federatedInviteeUser] as any[], ); - await this.rocketRoomAdapter.createFederatedRoomForDirectMessage(newFederatedRoom); + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); } const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom; if (isInviteeFromTheSameHomeServer) { await this.bridge.createUser( inviteeUsernameOnly, - federatedInviteeUser.internalReference.name as string, + federatedInviteeUser?.internalReference?.name || normalizedInviteeId, this.rocketSettingsAdapter.getHomeServerDomain(), ); await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId); @@ -112,7 +112,6 @@ export class FederationRoomServiceSender { if (!federatedRoom) { throw new Error(`Could not find room id for ${internalRoomId}`); } - await this.bridge.sendMessage(federatedRoom.externalId, federatedSender.externalId, message.msg); return message; @@ -126,4 +125,26 @@ export class FederationRoomServiceSender { return Boolean(federatedRoom?.isFederated()); } + + public canAddThisUserToTheRoom(internalUser: IUser | string, internalRoom: IRoom): void { + const newUserBeingAdded = typeof internalUser === 'string'; + if (newUserBeingAdded) { + return; + } + + if ((internalUser as IUser).federated && !internalRoom.federated) { + throw new Error('error-cant-add-federated-users'); + } + } + + public canAddUsersToTheRoom(internalUser: IUser | string, internalRoom: IRoom): void { + const newUserBeingAdded = typeof internalUser === 'string'; + if (newUserBeingAdded) { + return; + } + + if ((internalUser as IUser).federated && internalRoom.federated && internalRoom.t !== RoomType.DIRECT_MESSAGE) { + throw new Error('error-this-is-an-ee-feature'); + } + } } diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts index d9083e020e38..96d581402240 100644 --- a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -50,6 +50,10 @@ export class FederatedRoom { return this.internalReference?.federated === true; } + public isDirectMessage(): boolean { + return this.internalReference?.t === RoomType.DIRECT_MESSAGE; + } + public static buildRoomIdForDirectMessages(inviter: FederatedUser, invitee: FederatedUser): string { return inviter.internalReference?._id + invitee.internalReference?._id; } diff --git a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts index 61ca8cf47aac..c1b887290dd3 100644 --- a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -4,7 +4,7 @@ export interface IFederationBridge { onFederationAvailabilityChanged(enabled: boolean): Promise; getUserProfileInformation(externalUserId: string): Promise; joinRoom(externalRoomId: string, externalUserId: string): Promise; - createDirectMessageRoom(externalCreatorId: string, externalInviteeId: string): Promise; + createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: 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; diff --git a/apps/meteor/app/federation-v2/server/index.ts b/apps/meteor/app/federation-v2/server/index.ts index 2852970cde8f..9d8a085f344a 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,6 +1,5 @@ import { FederationFactoryEE } from '../../../ee/app/federation-v2/server/infrastructure/Factory'; import { FederationFactory } from './infrastructure/Factory'; -import './infrastructure/rocket-chat/slash-commands'; export const FEDERATION_PROCESSING_CONCURRENCY = 1; @@ -20,7 +19,7 @@ const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver federation, ); -const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver); +const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver, rocketSettingsAdapter); export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( rocketRoomAdapter, @@ -35,11 +34,13 @@ export const runFederation = async (): Promise => { await federation.start(); await rocketSettingsAdapter.onFederationEnabledStatusChanged(federation.onFederationAvailabilityChanged.bind(federation)); + require('./infrastructure/rocket-chat/slash-commands'); FederationFactory.setupListeners(federationRoomServiceSender); }; export const stopFederation = async (): Promise => { + FederationFactory.removeListeners(); await federation.stop(); }; diff --git a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts index 12d238a74d2e..e61cd3a34e56 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -66,14 +66,20 @@ export class FederationFactory { ); } - public static buildEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): MatrixEventsHandler { - return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive)); + public static buildEventHandlers( + roomServiceReceive: FederationRoomServiceReceiver, + rocketSettingsAdapter: RocketChatSettingsAdapter, + ): MatrixEventsHandler { + return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive, rocketSettingsAdapter)); } - public static getEventHandlers(roomServiceReceive: FederationRoomServiceReceiver): any[] { + public static getEventHandlers( + roomServiceReceive: FederationRoomServiceReceiver, + rocketSettingsAdapter: RocketChatSettingsAdapter, + ): any[] { return [ new MatrixRoomCreatedHandler(roomServiceReceive), - new MatrixRoomMembershipChangedHandler(roomServiceReceive), + new MatrixRoomMembershipChangedHandler(roomServiceReceive, rocketSettingsAdapter), new MatrixRoomMessageSentHandler(roomServiceReceive), ]; } @@ -82,5 +88,11 @@ export class FederationFactory { FederationHooks.afterLeaveRoom(async (user: IUser, room: IRoom) => roomServiceSender.leaveRoom(FederationRoomSenderConverter.toAfterLeaveRoom(user._id, room._id)), ); + FederationHooks.canAddTheUserToTheRoom((user: IUser | string, room: IRoom) => roomServiceSender.canAddThisUserToTheRoom(user, room)); + FederationHooks.canAddUsersToTheRoom((user: IUser | string, room: IRoom) => roomServiceSender.canAddUsersToTheRoom(user, room)); + } + + public static removeListeners(): void { + FederationHooks.removeCEValidation(); } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts index 98a3204c7621..b3bbc120f5b5 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -44,8 +44,6 @@ export class MatrixBridge implements IFederationBridge { } catch (e) { bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); - - // await this.settingsAdapter.disableFederation(); } } @@ -53,14 +51,14 @@ export class MatrixBridge implements IFederationBridge { if (!this.isRunning) { return; } + this.isRunning = false; // the http server might take some minutes to shutdown, and this promise can take some time to be resolved await this.bridgeInstance?.close(); - this.isRunning = false; } public async getUserProfileInformation(externalUserId: string): Promise { try { - return this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId); + return await this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId); } catch (err) { // no-op } @@ -71,7 +69,11 @@ export class MatrixBridge implements IFederationBridge { } public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise { - await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + try { + await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + } catch (e) { + // no-op + } } public async createUser(username: string, name: string, domain: string): Promise { @@ -84,12 +86,11 @@ export class MatrixBridge implements IFederationBridge { return matrixUserId; } - public async createDirectMessageRoom(externalCreatorId: string, externalInviteeId: string): Promise { + public async createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: string[]): Promise { const intent = this.bridgeInstance.getIntent(externalCreatorId); const visibility = RoomJoinRules.INVITE; const preset = MatrixRoomType.PRIVATE; - const matrixRoom = await intent.createRoom({ createAsClient: true, options: { @@ -97,7 +98,7 @@ export class MatrixBridge implements IFederationBridge { preset, // eslint-disable-next-line @typescript-eslint/camelcase is_direct: true, - invite: [externalInviteeId], + invite: externalInviteeIds, // eslint-disable-next-line @typescript-eslint/camelcase creation_content: { // eslint-disable-next-line @typescript-eslint/camelcase @@ -105,7 +106,6 @@ export class MatrixBridge implements IFederationBridge { }, }, }); - return matrixRoom.room_id; } @@ -148,9 +148,6 @@ export class MatrixBridge implements IFederationBridge { registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput), disableStores: true, controller: { - onAliasQuery: (alias, matrixRoomId): void => { - console.log('onAliasQuery', alias, matrixRoomId); - }, onEvent: async (request /* , context*/): Promise => { // Get the event const event = request.getData() as unknown as IMatrixEvent; 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 index 463b460d111f..0c5da51c3be7 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -26,6 +26,7 @@ export class MatrixRoomReceiverConverter { public static toChangeRoomMembershipDto( externalEvent: IMatrixEvent, + homeServerDomain: string, ): FederationRoomChangeMembershipDto { return Object.assign(new FederationRoomChangeMembershipDto(), { ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), @@ -39,7 +40,7 @@ export class MatrixRoomReceiverConverter { normalizedInviteeId: MatrixRoomReceiverConverter.convertMatrixUserIdFormatToRCFormat(externalEvent.state_key), inviteeUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.state_key), inviterUsernameOnly: MatrixRoomReceiverConverter.formatMatrixUserIdToRCUsernameFormat(externalEvent.sender), - eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, externalEvent.state_key), + eventOrigin: MatrixRoomReceiverConverter.getEventOrigin(externalEvent.sender, homeServerDomain), leave: externalEvent.content?.membership === AddMemberToRoomMembership.LEAVE, }); } @@ -68,10 +69,8 @@ export class MatrixRoomReceiverConverter { return matrixUserId.split(':')[0]?.replace('@', ''); } - protected static getEventOrigin(inviterId = '', inviteeId = ''): EVENT_ORIGIN { - const fromADifferentServer = - MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== - MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviteeId); + protected static getEventOrigin(inviterId = '', homeServerDomain: string): EVENT_ORIGIN { + const fromADifferentServer = MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== homeServerDomain; return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL; } @@ -90,13 +89,15 @@ export class MatrixRoomReceiverConverter { } protected static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType { + if (matrixRoomIsDirect) { + return RoomType.DIRECT_MESSAGE; + } 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; + return mapping[matrixJoinRule] || RoomType.CHANNEL; } protected static tryToGetExternalInfoFromTheRoomState( 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 index 8c896fd22b56..e6122c567675 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -1,4 +1,5 @@ import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver'; +import { RocketChatSettingsAdapter } from '../../rocket-chat/adapters/Settings'; import { MatrixRoomReceiverConverter } from '../converters/RoomReceiver'; import { IMatrixEvent } from '../definitions/IMatrixEvent'; import { MatrixEventType } from '../definitions/MatrixEventType'; @@ -15,12 +16,14 @@ export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler { - constructor(private roomService: FederationRoomServiceReceiver) { + constructor(private roomService: FederationRoomServiceReceiver, private rocketSettingsAdapter: RocketChatSettingsAdapter) { super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED); } public async handle(externalEvent: IMatrixEvent): Promise { - await this.roomService.changeRoomMembership(MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent)); + await this.roomService.changeRoomMembership( + MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent, this.rocketSettingsAdapter.getHomeServerDomain()), + ); } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts index 2d452d5f9374..215c0503548a 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts @@ -2,7 +2,12 @@ import { IRoom, ValueOf } from '@rocket.chat/core-typings'; import { RoomMemberActions } from '../../../../../definition/IRoomTypeConfig'; -const allowedActionsInFederatedRooms: ValueOf[] = [RoomMemberActions.REMOVE_USER]; +const allowedActionsInFederatedRooms: ValueOf[] = [ + RoomMemberActions.REMOVE_USER, + RoomMemberActions.INVITE, + RoomMemberActions.JOIN, + RoomMemberActions.LEAVE, +]; export class Federation { public static isAFederatedRoom(room: IRoom): boolean { @@ -12,4 +17,8 @@ export class Federation { public static federationActionAllowed(action: ValueOf): boolean { return allowedActionsInFederatedRooms.includes(action); } + + public static isAFederatedUsername(username: string): boolean { + return username.includes('@') && username.includes(':'); + } } 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 index 637e5315531c..d62e7e91fe3b 100644 --- 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 @@ -1,7 +1,7 @@ import { ICreatedRoom, IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; -import { MatrixBridgedRoom } from '../../../../../models/server'; +import { MatrixBridgedRoom, Subscriptions } from '../../../../../models/server'; import { FederatedRoom } from '../../../domain/FederatedRoom'; import { createRoom, addUserToRoom, removeUserFromRoom } from '../../../../../lib/server'; import { FederatedUser } from '../../../domain/FederatedUser'; @@ -41,22 +41,31 @@ export class RocketChatRoomAdapter { federatedRoom.internalReference.name, federatedRoom.internalReference.u.username as string, members, + false, + undefined, + { creator: members[0]?._id as string }, ) as ICreatedRoom; const roomId = rid || _id; - MatrixBridgedRoom.insert({ rid: roomId, mri: federatedRoom.externalId }); + MatrixBridgedRoom.upsert({ rid: roomId }, { rid: roomId, mri: federatedRoom.externalId }); await Rooms.setAsFederated(roomId); } - public async createFederatedRoomForDirectMessage(federatedRoom: FederatedRoom): Promise { - const members = federatedRoom.getMembers(); + public async removeDirectMessageRoom(federatedRoom: FederatedRoom): Promise { + const roomId = federatedRoom.internalReference._id; + await Rooms.removeById(roomId); + await Subscriptions.removeByRoomId(roomId); + await MatrixBridgedRoom.remove({ rid: roomId }); + } + + public async createFederatedRoomForDirectMessage(federatedRoom: FederatedRoom, membersUsernames: string[]): Promise { const { rid, _id } = createRoom( federatedRoom.internalReference.t, federatedRoom.internalReference.name, federatedRoom.internalReference.u.username as string, - members, + membersUsernames, false, undefined, - { creator: members[0]?._id as string }, + { creator: federatedRoom.internalReference.u._id }, ) as ICreatedRoom; const roomId = rid || _id; MatrixBridgedRoom.upsert({ rid: roomId }, { rid: roomId, mri: federatedRoom.externalId }); 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 index 72f94562b537..5ede953e29b7 100644 --- 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 @@ -129,6 +129,7 @@ export class RocketChatSettingsAdapter { i18nLabel: 'Federation_Matrix_enabled', i18nDescription: 'Federation_Matrix_enabled_desc', alert: 'Federation_Matrix_Enabled_Alert', + public: true, }); const uniqueId = settings.get('uniqueID') || uuidv4().slice(0, 15).replace(new RegExp('-', 'g'), '_'); 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 index a2f2c26f8e6c..7a483c05f1f6 100644 --- 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 @@ -1,6 +1,7 @@ import { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; -import { MatrixBridgedUser, Users } from '../../../../../models/server'; +import { MatrixBridgedUser } from '../../../../../models/server'; import { FederatedUser } from '../../../domain/FederatedUser'; export class RocketChatUserAdapter { @@ -12,7 +13,9 @@ export class RocketChatUserAdapter { const user = await Users.findOneById(internalBridgedUserId); - return this.createFederatedUserInstance(externalUserId, user); + if (user) { + return this.createFederatedUserInstance(externalUserId, user); + } } public async getFederatedUserByInternalId(internalUserId: string): Promise { @@ -23,7 +26,9 @@ export class RocketChatUserAdapter { const { uid: userId, mui: externalUserId } = internalBridgedUserId; const user = await Users.findOneById(userId); - return this.createFederatedUserInstance(externalUserId, user); + if (user) { + return this.createFederatedUserInstance(externalUserId, user); + } } public async getFederatedUserByInternalUsername(username: string): Promise { @@ -40,13 +45,14 @@ export class RocketChatUserAdapter { return this.createFederatedUserInstance(externalUserId, user); } - public async getInternalUserById(userId: string): Promise { + public async getInternalUserById(userId: string): Promise { return Users.findOneById(userId); } public async createFederatedUser(federatedUser: FederatedUser): Promise { - const existingLocalUser = await Users.findOneByUsername(federatedUser.internalReference.username); + const existingLocalUser = await Users.findOneByUsername(federatedUser.internalReference.username || ''); if (existingLocalUser) { + await Users.setAsFederated(existingLocalUser._id); return MatrixBridgedUser.upsert( { uid: existingLocalUser._id }, { @@ -56,7 +62,7 @@ export class RocketChatUserAdapter { }, ); } - const newLocalUserId = await Users.create({ + const { insertedId } = await Users.insertOne({ username: federatedUser.internalReference.username, type: federatedUser.internalReference.type, status: federatedUser.internalReference.status, @@ -64,11 +70,13 @@ export class RocketChatUserAdapter { roles: federatedUser.internalReference.roles, name: federatedUser.internalReference.name, requirePasswordChange: federatedUser.internalReference.requirePasswordChange, + createdAt: new Date(), + federated: true, }); MatrixBridgedUser.upsert( - { uid: newLocalUserId }, + { uid: insertedId }, { - uid: newLocalUserId, + uid: insertedId, mui: federatedUser.externalId, remote: !federatedUser.existsOnlyOnProxyServer, }, diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts index 5c406430c8e7..3db1f393943b 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts @@ -16,4 +16,30 @@ export class FederationHooks { 'federation-v2-after-leave-room', ); } + + public static canAddTheUserToTheRoom(callback: Function): void { + callbacks.add( + 'federation.beforeAddUserAToRoom', + (params: { user: IUser | string }, room: IRoom): void => { + Promise.await(callback(params.user, room)); + }, + callbacks.priority.HIGH, + 'federation-v2-can-add-user-to-the-room', + ); + } + + public static canAddUsersToTheRoom(callback: Function): void { + callbacks.add( + 'federation.beforeAddUserAToRoom', + (params: { user: IUser | string }, room: IRoom): void => { + Promise.await(callback(params.user, room)); + }, + callbacks.priority.HIGH, + 'federation-v2-can-add-users-to-the-room', + ); + } + + public static removeCEValidation(): void { + callbacks.remove('federation.beforeAddUserAToRoom', 'federation-v2-can-add-users-to-the-room'); + } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts index bb31a63c468c..46b7e4425b89 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; +import { Users } from '@rocket.chat/models'; import { federationRoomServiceSender } from '../../..'; import { FederationRoomSenderConverter } from '../converters/RoomSender'; import { slashCommands } from '../../../../../utils/lib/slashCommand'; -export const FEDERATION_COMMANDS: Record = { +const FEDERATION_COMMANDS: Record = { dm: async (currentUserId: string, roomId: string, invitee: string) => federationRoomServiceSender.createDirectMessageRoomAndInviteUser( FederationRoomSenderConverter.toCreateDirectMessageRoomDto(currentUserId, roomId, invitee), @@ -12,13 +13,16 @@ export const FEDERATION_COMMANDS: Record = { }; export const normalizeUserId = (rawUserId: string): string => `@${rawUserId.replace('@', '')}`; -export const validateUserIdFormat = (rawUserId: string) => { - if (!rawUserId.includes(':')) { + +const validateUserIdFormat = async (rawUserId: string, inviterId: string) => { + const inviter = await Users.findOneById(inviterId); + const isInviterExternal = inviter?.federated === true || inviter?.username?.includes(':'); + if (!rawUserId.includes(':') && !isInviterExternal) { throw new Error('Invalid userId format for federation command.'); } }; -export const executeSlashCommand = async ( +const executeSlashCommand = async ( providedCommand: string, stringParams: string | undefined, item: Record, @@ -28,19 +32,19 @@ export const executeSlashCommand = async ( return; } - const [command, ...params] = stringParams.split(' '); + const [command, ...params] = stringParams.trim().split(' '); const [rawUserId] = params; - validateUserIdFormat(rawUserId); - const currentUserId = Meteor.userId(); - const invitee = normalizeUserId(rawUserId); - - const { rid: roomId } = item; - if (!currentUserId || !commands[command]) { return; } + await validateUserIdFormat(rawUserId, currentUserId); + + const invitee = normalizeUserId(rawUserId); + + const { rid: roomId } = item; + await commands[command](currentUserId, roomId, invitee); }; diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 0bdaa33c8193..668b9c64f54e 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -4,14 +4,14 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { AppEvents, Apps } from '../../../apps/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Messages, Rooms, Subscriptions } from '../../../models/server'; +import { Messages, Rooms, Subscriptions, Users } from '../../../models/server'; import { Team } from '../../../../server/sdk'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; export const addUserToRoom = function ( rid: string, - user: Pick, + user: Pick | string, inviter?: Pick, silenced?: boolean, ): boolean | unknown { @@ -26,14 +26,24 @@ export const addUserToRoom = function ( return; } + try { + callbacks.run('federation.beforeAddUserAToRoom', { user, inviter }, room); + } catch (error) { + throw new Meteor.Error((error as any)?.message); + } + + const username = typeof user === 'string' ? user.replace('@', '') : user.username; + + const userToBeAdded = Users.findOneByUsername(username); + // Check if user is already in room - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); if (subscription) { return; } try { - Promise.await(Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter)); + Promise.await(Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter)); } catch (error) { if (error instanceof AppsEngineException) { throw new Meteor.Error('error-app-prevented', error.message); @@ -44,14 +54,14 @@ export const addUserToRoom = function ( if (room.t === 'c' || room.t === 'p' || room.t === 'l') { // Add a new event, with an optional inviter - callbacks.run('beforeAddedToRoom', { user, inviter }, room); + callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter }, room); // Keep the current event - callbacks.run('beforeJoinRoom', user, room); + callbacks.run('beforeJoinRoom', userToBeAdded, room); } Promise.await( - Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter).catch((error) => { + Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter).catch((error) => { if (error instanceof AppsEngineException) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -60,7 +70,7 @@ export const addUserToRoom = function ( }), ); - Subscriptions.createWithRoomAndUser(room, user, { + Subscriptions.createWithRoomAndUser(room, userToBeAdded, { ts: now, open: true, alert: true, @@ -79,34 +89,34 @@ export const addUserToRoom = function ( }, }; if (room.teamMain) { - Messages.createUserAddedToTeamWithRoomIdAndUser(rid, user, extraData); + Messages.createUserAddedToTeamWithRoomIdAndUser(rid, userToBeAdded, extraData); } else { - Messages.createUserAddedWithRoomIdAndUser(rid, user, extraData); + Messages.createUserAddedWithRoomIdAndUser(rid, userToBeAdded, extraData); } } else if (room.prid) { - Messages.createUserJoinWithRoomIdAndUserDiscussion(rid, user, { ts: now }); + Messages.createUserJoinWithRoomIdAndUserDiscussion(rid, userToBeAdded, { ts: now }); } else if (room.teamMain) { - Messages.createUserJoinTeamWithRoomIdAndUser(rid, user, { ts: now }); + Messages.createUserJoinTeamWithRoomIdAndUser(rid, userToBeAdded, { ts: now }); } else { - Messages.createUserJoinWithRoomIdAndUser(rid, user, { ts: now }); + Messages.createUserJoinWithRoomIdAndUser(rid, userToBeAdded, { ts: now }); } } if (room.t === 'c' || room.t === 'p') { Meteor.defer(function () { // Add a new event, with an optional inviter - callbacks.run('afterAddedToRoom', { user, inviter }, room); + callbacks.run('afterAddedToRoom', { user: userToBeAdded, inviter }, room); // Keep the current event - callbacks.run('afterJoinRoom', user, room); + callbacks.run('afterJoinRoom', userToBeAdded, room); - Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, user, inviter); + Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); }); } if (room.teamMain && room.teamId && inviter) { // if user is joining to main team channel, create a membership - Promise.await(Team.addMember(inviter, user._id, room.teamId)); + Promise.await(Team.addMember(inviter, userToBeAdded._id, room.teamId)); } return true; diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 1f2f454f9a7d..0e3c6c12d6fb 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -2,11 +2,11 @@ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/excepti import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import type { IUser } from '@rocket.chat/core-typings'; -import { Users, Subscriptions } from '@rocket.chat/models'; +import { Subscriptions } from '@rocket.chat/models'; +import { Users, Rooms } from '../../../models/server'; import { Apps } from '../../../apps/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/server'; import { ICreateRoomParams } from '../../../../server/sdk/types/IRoomService'; @@ -32,16 +32,27 @@ const generateSubscription = (fname: string, name: string, user: IUser, extra: { const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); const getName = (members: IUser[]): string => members.map(({ username }) => username).join(', '); -export const createDirectRoom = function (members: IUser[], roomExtraData = {}, options: ICreateRoomParams['options']): unknown { +export const createDirectRoom = function (members: IUser[] | string[], roomExtraData = {}, options: ICreateRoomParams['options']): unknown { if (members.length > (settings.get('DirectMesssage_maxUsers') || 1)) { throw new Error('error-direct-message-max-user-exceeded'); } + callbacks.run('beforeCreateDirectRoom', members); + const membersUsernames = members.map((member) => { + if (typeof member === 'string') { + return member.replace('@', ''); + } + return member.username; + }); + + const roomMembers: IUser[] = Users.findUsersByUsernames(membersUsernames, { + fields: { _id: 1, name: 1, username: 1, settings: 1, customFields: 1 }, + }).fetch(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const sortedMembers = members.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); + const sortedMembers = roomMembers.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); const usernames = sortedMembers.map(({ username }) => username); - const uids = members.map(({ _id }) => _id).sort(); + const uids = roomMembers.map(({ _id }) => _id).sort(); // Deprecated: using users' _id to compose the room _id is deprecated const room = @@ -92,22 +103,25 @@ export const createDirectRoom = function (members: IUser[], roomExtraData = {}, const rid = room?._id || Rooms.insert(roomInfo); - if (members.length === 1) { + if (roomMembers.length === 1) { // dm to yourself Subscriptions.updateOne( - { rid, 'u._id': members[0]._id }, + { rid, 'u._id': roomMembers[0]._id }, { $set: { open: true }, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - $setOnInsert: generateSubscription(members[0].name! || members[0].username!, members[0].username!, members[0], { + $setOnInsert: generateSubscription(roomMembers[0].name! || roomMembers[0].username!, roomMembers[0].username!, roomMembers[0], { ...options?.subscriptionExtra, }), }, { upsert: true }, ); } else { - const memberIds = members.map((member) => member._id); - const membersWithPreferences = Users.find({ _id: { $in: memberIds } }, { projection: { 'username': 1, 'settings.preferences': 1 } }); + const memberIds = roomMembers.map((member) => member._id); + const membersWithPreferences: IUser[] = Users.find( + { _id: { $in: memberIds } }, + { projection: { 'username': 1, 'settings.preferences': 1 } }, + ); membersWithPreferences.forEach((member) => { const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); @@ -129,7 +143,7 @@ export const createDirectRoom = function (members: IUser[], roomExtraData = {}, if (isNewRoom) { const insertedRoom = Rooms.findOneById(rid); - callbacks.run('afterCreateDirectRoom', insertedRoom, { members }); + callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator }); Apps.triggerEvent('IPostRoomCreate', insertedRoom); } diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 903d942dba19..3af15bce7f84 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -31,7 +31,7 @@ export const createRoom = function ( callbacks.run('beforeCreateRoom', { type, name, owner: ownerUsername, members, readOnly, extraData, options }); if (type === 'd') { - return createDirectRoom(members as IUser[], extraData, options); + return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername }); } if (!isValidName(name)) { @@ -114,28 +114,40 @@ export const createRoom = function ( callbacks.run('beforeCreateChannel', owner, roomProps); } const room = Rooms.createWithFullRoomData(roomProps); - - for (const username of [...new Set(members as string[])]) { - const member = Users.findOneByUsername(username, { - fields: { 'username': 1, 'settings.preferences': 1 }, - }); - if (!member) { - continue; - } - + const shouldBeHandledByFederation = room.federated === true || ownerUsername.includes(':'); + if (shouldBeHandledByFederation) { const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; + extra.ls = now; if (room.prid) { extra.prid = room.prid; } - if (username === owner.username) { - extra.ls = now; - } + Subscriptions.createWithRoomAndUser(room, owner, extra); + } else { + for (const username of [...new Set(members as string[])]) { + const member = Users.findOneByUsername(username, { + fields: { 'username': 1, 'settings.preferences': 1, 'federated': 1 }, + }); + if (!member || member?.federated) { + continue; + } + + const extra: Partial = options?.subscriptionExtra || {}; - Subscriptions.createWithRoomAndUser(room, member, extra); + extra.open = true; + + if (room.prid) { + extra.prid = room.prid; + } + + if (username === owner.username) { + extra.ls = now; + } + + Subscriptions.createWithRoomAndUser(room, member, extra); + } } addUserRoles(owner._id, ['owner'], room._id); @@ -150,6 +162,9 @@ export const createRoom = function ( callbacks.runAsync('afterCreatePrivateGroup', owner, room); } callbacks.runAsync('afterCreateRoom', owner, room); + if (shouldBeHandledByFederation) { + callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members as string[] }); + } Apps.triggerEvent('IPostRoomCreate', room); diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.js b/apps/meteor/app/lib/server/methods/addUsersToRoom.js index 0fd3fab71681..99c5db5d011b 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.js +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.js @@ -6,6 +6,7 @@ import { Rooms, Subscriptions, Users } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { addUserToRoom } from '../functions'; import { api } from '../../../../server/sdk/api'; +import { Federation } from '../../../federation-v2/server/infrastructure/rocket-chat/Federation'; Meteor.methods({ addUsersToRoom(data = {}) { @@ -65,14 +66,14 @@ Meteor.methods({ const user = Meteor.user(); data.users.forEach((username) => { const newUser = Users.findOneByUsernameIgnoringCase(username); - if (!newUser) { + if (!newUser && !Federation.isAFederatedUsername(username)) { throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'addUsersToRoom', }); } - const subscription = Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id); + const subscription = newUser && Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id); if (!subscription) { - addUserToRoom(data.rid, newUser, user); + addUserToRoom(data.rid, newUser || username, user); } else { api.broadcast('notify.ephemeralMessage', userId, data.rid, { msg: TAPi18n.__( diff --git a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx index 22bcd3cde2d3..888dcb6d3866 100644 --- a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx +++ b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx @@ -1,8 +1,8 @@ -import { IRoom, isDirectMessageRoom, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { IRoom, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Icon } from '@rocket.chat/fuselage'; -import React, { ComponentProps, ReactElement } from 'react'; +import React, { ComponentProps, ReactElement, isValidElement } from 'react'; -import { ReactiveUserStatus } from '../UserStatus'; +import { useRoomIcon } from '../../hooks/useRoomIcon'; import { OmnichannelRoomIcon } from './OmnichannelRoomIcon'; export const RoomIcon = ({ @@ -14,33 +14,19 @@ export const RoomIcon = ({ size?: ComponentProps['size']; placement: 'sidebar' | 'default'; }): ReactElement | null => { - if (room.prid) { - return ; - } - - if (room.teamMain) { - return ; - } + const iconPropsOrReactNode = useRoomIcon(room); if (isOmnichannelRoom(room)) { return ; } - if (isDirectMessageRoom(room)) { - if (room.uids && room.uids.length > 2) { - return ; - } - if (room.uids && room.uids.length > 0) { - return uid !== room.u._id)[0] || room.u._id} />; - } - return ; + + if (isValidElement(iconPropsOrReactNode)) { + return iconPropsOrReactNode; } - switch (room.t) { - case 'p': - return ; - case 'c': - return ; - default: - return null; + if (!iconPropsOrReactNode) { + return null; } + + return ; }; diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx new file mode 100644 index 000000000000..470331f3803c --- /dev/null +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx @@ -0,0 +1,89 @@ +import { MultiSelectFiltered, Icon, Box, Chip } from '@rocket.chat/fuselage'; +import type { Options } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement, useState, ComponentProps } from 'react'; +import { useQuery } from 'react-query'; + +import UserAvatar from '../avatar/UserAvatar'; +import renderOptions from './UserAutoCompleteMultipleOptions'; + +type UserAutoCompleteMultipleFederatedProps = { + onChange: (value: Array) => void; + value: Array; + placeholder?: string; +}; + +export type UserAutoCompleteOptionType = { + name: string; + username: string; + _federated?: boolean; +}; + +type UserAutoCompleteOptions = { + [k: string]: UserAutoCompleteOptionType; +}; + +const matrixRegex = new RegExp('(.*:.*)', 'gi'); + +const UserAutoCompleteMultipleFederated = ({ + onChange, + value, + placeholder, + ...props +}: UserAutoCompleteMultipleFederatedProps): ReactElement => { + const [filter, setFilter] = useState(''); + const [selectedCache, setSelectedCache] = useState({}); + + const debouncedFilter = useDebouncedValue(filter, 1000); + const getUsers = useEndpoint('GET', '/v1/users.autocomplete'); + + const { data } = useQuery(['users.autocomplete', debouncedFilter], async () => { + const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) }); + const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]); + + // Add extra option if filter text matches `username:server` + // Used to add federated users that do not exist yet + if (matrixRegex.test(debouncedFilter)) { + options.unshift([debouncedFilter, { name: debouncedFilter, username: debouncedFilter, _federated: true }]); + } + + return options; + }); + + const options = data || []; + + const onAddSelected: ComponentProps['onSelect'] = ([value]) => { + const cachedOption = options.find(([curVal]) => curVal === value)?.[1]; + if (!cachedOption) { + throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option'); + } + setSelectedCache({ ...selectedCache, [value]: cachedOption }); + }; + + return ( + void }): ReactElement => { + const currentCachedOption = selectedCache[value]; + + return ( + + {currentCachedOption._federated ? : } + + {currentCachedOption.name || currentCachedOption.username} + + + ); + }} + renderOptions={renderOptions(options, onAddSelected)} + options={options.concat(Object.entries(selectedCache)).map(([, item]) => [item.username, item.name || item.username])} + /> + ); +}; + +export default memo(UserAutoCompleteMultipleFederated); diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx new file mode 100644 index 000000000000..e8be27f28c34 --- /dev/null +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOption.tsx @@ -0,0 +1,31 @@ +import { IUser } from '@rocket.chat/core-typings'; +import { Option, OptionDescription } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +import UserAvatar from '../avatar/UserAvatar'; + +type UserAutoCompleteMultipleOptionProps = { + label: { + _federated?: boolean; + } & Pick; +}; + +const UserAutoCompleteMultipleOption = ({ label, ...props }: UserAutoCompleteMultipleOptionProps): ReactElement => { + const { name, username, _federated } = label; + + return ( +