diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 2e0f34c0e1f31..7f26a78d85089 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -71,9 +71,7 @@ export async function createDirectRoom( await callbacks.run('beforeCreateDirectRoom', membersUsernames, roomExtraData); - const roomMembers: IUser[] = await Users.findUsersByUsernames(membersUsernames, { - projection: { _id: 1, name: 1, username: 1, settings: 1, customFields: 1 }, - }).toArray(); + const roomMembers = await Users.findUsersByUsernames(membersUsernames).toArray(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const sortedMembers = roomMembers.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index 43c073e037667..086221803d87e 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -1,12 +1,10 @@ import { api } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../functions/addUserToRoom'; @@ -79,12 +77,6 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; }); } - // Validate each user, then add to room - if (isRoomFederated(room)) { - await callbacks.run('federation.onAddUsersToRoom', { invitees: data.users, inviter: user }, room); - return true; - } - await Promise.all( data.users.map(async (username) => { const newUser = await Users.findOneByUsernameIgnoringCase(username); diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 33d76a54b4ca5..7d2adb93dece1 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -70,21 +70,6 @@ callbacks.add( 'native-federation-after-delete-message', ); -callbacks.add( - 'federation.onAddUsersToRoom', - async ({ invitees, inviter }, room) => { - if (FederationActions.shouldPerformFederationAction(room)) { - await FederationMatrix.inviteUsersToRoom( - room, - invitees.map((invitee) => (typeof invitee === 'string' ? invitee : invitee.username)).filter((v) => v != null), - inviter, - ); - } - }, - callbacks.priority.MEDIUM, - 'native-federation-on-add-users-to-room ', -); - beforeAddUserToRoom.add( async ({ user, inviter }, room) => { if (!user.username || !inviter) { diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 7fe2240f2342e..30d0cef5d5e77 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -11,7 +11,6 @@ import type { VideoConference, OEmbedMeta, OEmbedUrlContent, - Username, IOmnichannelRoom, ILivechatTag, ILivechatTagRecord, @@ -85,7 +84,6 @@ interface EventLikeCallbackSignatures { message: IMessage, params: { user: IUser; reaction: string; shouldReact: boolean; oldMessage: IMessage; room: IRoom }, ) => void; - 'federation.onAddUsersToRoom': (params: { invitees: IUser[] | Username[]; inviter: IUser }, room: IRoom) => void; 'onJoinVideoConference': (callId: VideoConference['_id'], userId?: IUser['_id']) => Promise; 'usernameSet': () => void; 'beforeJoinRoom': (user: IUser, room: IRoom) => void; diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index c0cd23b53e4a0..70a332683f9b3 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -24,7 +24,7 @@ export const addRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom['_id check(rid, String); check(userId, String); - const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } }); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'addRoomModerator', diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index 36d65c6796803..b60779d648ca7 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -24,7 +24,7 @@ export const addRoomOwner = async (fromUserId: IUser['_id'], rid: IRoom['_id'], check(rid, String); check(userId, String); - const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } }); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'addRoomOwner', diff --git a/apps/meteor/server/methods/removeRoomModerator.ts b/apps/meteor/server/methods/removeRoomModerator.ts index db7808d592d7c..a9a7c482c87cc 100644 --- a/apps/meteor/server/methods/removeRoomModerator.ts +++ b/apps/meteor/server/methods/removeRoomModerator.ts @@ -23,7 +23,7 @@ export const removeRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom[' check(rid, String); check(userId, String); - const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } }); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'removeRoomModerator', diff --git a/apps/meteor/server/methods/removeRoomOwner.ts b/apps/meteor/server/methods/removeRoomOwner.ts index f39d81ba31f0d..555879ac5132f 100644 --- a/apps/meteor/server/methods/removeRoomOwner.ts +++ b/apps/meteor/server/methods/removeRoomOwner.ts @@ -22,7 +22,7 @@ export const removeRoomOwner = async (fromUserId: string, rid: string, userId: s check(rid, String); check(userId, String); - const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } }); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'removeRoomOwner', diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 1730a9a2400d4..2b5ed757368aa 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -14,6 +14,7 @@ import { Logger } from '@rocket.chat/logger'; import { Users, Subscriptions, Messages, Rooms, Settings } from '@rocket.chat/models'; import emojione from 'emojione'; +import { acceptInvite } from './api/_matrix/invite'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; import { MatrixMediaService } from './services/MatrixMediaService'; @@ -24,6 +25,119 @@ export const fileTypes: Record = { file: 'm.file', }; +/** helper to validate the username format */ +export function validateFederatedUsername(mxid: string): mxid is `@${string}:${string}` { + if (!mxid.startsWith('@')) return false; + + const parts = mxid.substring(1).split(':'); + if (parts.length < 2) return false; + + const localpart = parts[0]; + const domainAndPort = parts.slice(1).join(':'); + + const localpartRegex = /^(?:[a-z0-9._\-]|=[0-9a-fA-F]{2}){1,255}$/; + if (!localpartRegex.test(localpart)) return false; + + const [domain, port] = domainAndPort.split(':'); + + const hostnameRegex = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?)*$/i; + const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/; + const ipv6Regex = /^\[([0-9a-f:.]+)\]$/i; + + if (!(hostnameRegex.test(domain) || ipv4Regex.test(domain) || ipv6Regex.test(domain))) { + return false; + } + + if (port !== undefined) { + const portNum = Number(port); + if (!/^[0-9]+$/.test(port) || portNum < 1 || portNum > 65535) { + return false; + } + } + + return true; +} +export const extractDomainFromMatrixUserId = (mxid: string): string => { + const separatorIndex = mxid.indexOf(':', 1); + if (separatorIndex === -1) { + throw new Error(`Invalid federated username: ${mxid}`); + } + return mxid.substring(separatorIndex + 1); +}; + +/** + * Extract the username and the servername from a matrix user id + * if the serverName is the same as the serverName in the mxid, return only the username (rocket.chat regular username) + * otherwise, return the full mxid and the servername + */ +export const getUsernameServername = (mxid: string, serverName: string): [mxid: string, serverName: string, isLocal: boolean] => { + const senderServerName = extractDomainFromMatrixUserId(mxid); + // if the serverName is the same as the serverName in the mxid, return only the username (rocket.chat regular username) + if (serverName === senderServerName) { + const separatorIndex = mxid.indexOf(':', 1); + if (separatorIndex === -1) { + throw new Error(`Invalid federated username: ${mxid}`); + } + return [mxid.substring(1, separatorIndex), senderServerName, true]; // removers also the @ + } + + return [mxid, senderServerName, false]; +}; +/** + * Helper function to create a federated user + * + * Because of historical reasons, we can have users only with federated flag but no federation object + * So we need to upsert the user with the federation object + */ +export async function createOrUpdateFederatedUser(options: { + username: `@${string}:${string}`; + name?: string; + origin: string; +}): Promise { + const { username, name = username, origin } = options; + + const result = await Users.updateOne( + { + username, + }, + { + $set: { + username, + name: name || username, + type: 'user' as const, + status: UserStatus.OFFLINE, + active: true, + roles: ['user'], + requirePasswordChange: false, + federated: true, + federation: { + version: 1, + mui: username, + origin, + }, + _updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + }, + ); + + const userId = result.upsertedId || (await Users.findOneByUsername(username, { projection: { _id: 1 } }))?._id; + if (!userId) { + throw new Error(`Failed to create or update federated user: ${username}`); + } + if (typeof userId !== 'string') { + return userId.toString(); + } + return userId; +} + +export { generateEd25519RandomSecretKey } from '@rocket.chat/federation-sdk'; + export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -152,41 +266,26 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS try { this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: usernames.length }); - const federatedUsers = usernames.filter((username) => username?.includes(':') && username?.includes('@')); + const federatedUsers = usernames.filter(validateFederatedUsername); for await (const username of federatedUsers) { - if (!username) { - continue; - } - const existingUser = await Users.findOneByUsername(username); - if (existingUser) { + if (existingUser && isUserNativeFederated(existingUser)) { continue; } - await Users.create({ + await createOrUpdateFederatedUser({ username, name: username, - type: 'user' as const, - status: UserStatus.OFFLINE, - active: true, - roles: ['user'], - requirePasswordChange: false, - federated: true, - federation: { - version: 1, - mui: username, - origin: username.split(':')[1], - }, - createdAt: new Date(), - _updatedAt: new Date(), + origin: extractDomainFromMatrixUserId(username), }); } } catch (error) { - this.logger.error('Failed to ensure federated users exist locally:', error); + this.logger.error({ msg: 'Failed to ensure federated users exist locally', error }); + throw error; } } - async createDirectMessageRoom(room: IRoom, members: (IUser | string)[], creatorId: IUser['_id']): Promise { + async createDirectMessageRoom(room: IRoom, members: IUser[], creatorId: IUser['_id']): Promise { try { this.logger.debug('Creating direct message room in Matrix', { roomId: room._id, memberCount: members.length }); @@ -204,24 +303,14 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS let matrixRoomResult: { room_id: string; event_id?: string }; if (members.length === 2) { - const otherMember = members.find((member) => { - if (typeof member === 'string') { - return true; // Remote user - } - return member._id !== creatorId; - }); + const otherMember = members.find((member) => member._id !== creatorId); if (!otherMember) { throw new Error('Other member not found for 1-on-1 DM'); } - let otherMemberMatrixId: string; - if (typeof otherMember === 'string') { - otherMemberMatrixId = otherMember.startsWith('@') ? otherMember : `@${otherMember}`; - } else if (otherMember.username?.includes(':')) { - otherMemberMatrixId = otherMember.username.startsWith('@') ? otherMember.username : `@${otherMember.username}`; - } else { - otherMemberMatrixId = `@${otherMember.username}:${this.serverName}`; + if (!isUserNativeFederated(otherMember)) { + throw new Error('Other member is not federated'); } - const roomId = await this.homeserverServices.room.createDirectMessageRoom(actualMatrixUserId, otherMemberMatrixId); + const roomId = await this.homeserverServices.room.createDirectMessageRoom(actualMatrixUserId, otherMember.username); matrixRoomResult = { room_id: roomId }; } else { // For group DMs (more than 2 members), create a private room @@ -229,59 +318,17 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS matrixRoomResult = await this.homeserverServices.room.createRoom(actualMatrixUserId, roomName, 'invite'); } - // TODO is this needed? - // const mapping = await MatrixBridgedRoom.getLocalRoomId(matrixRoomResult.room_id); - // if (!mapping) { - // await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, this.serverName); - // } - for await (const member of members) { - if (typeof member !== 'string' && member._id === creatorId) { + if (member._id === creatorId) { continue; } - try { - let memberMatrixUserId: string; - let memberId: string | undefined; - - if (typeof member === 'string') { - memberMatrixUserId = member.startsWith('@') ? member : `@${member}`; - memberId = undefined; - } else if (member.username?.includes(':')) { - memberMatrixUserId = member.username.startsWith('@') ? member.username : `@${member.username}`; - memberId = member._id; - } else { - continue; - } - - if (memberId) { - const existingMemberMatrixUserId = await Users.findOne({ 'federation.mui': memberId }); - if (!existingMemberMatrixUserId) { - const newUser = { - username: memberId, - name: memberId, - type: 'user' as const, - status: UserStatus.OFFLINE, - active: true, - roles: ['user'], - requirePasswordChange: false, - federated: true, - federation: { - version: 1, - mui: memberId, - origin: memberMatrixUserId.split(':').pop(), - }, - createdAt: new Date(), - _updatedAt: new Date(), - }; - - await Users.insertOne(newUser); - } - } + if (!isUserNativeFederated(member)) { + continue; + } - if (members.length > 2) { - await this.homeserverServices.invite.inviteUserToRoom(memberMatrixUserId, matrixRoomResult.room_id, actualMatrixUserId); - } + try { + await this.homeserverServices.invite.inviteUserToRoom(member.username, matrixRoomResult.room_id, actualMatrixUserId); } catch (error) { this.logger.error('Error creating or updating bridged user for DM:', error); } @@ -535,24 +582,31 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - async inviteUsersToRoom(room: IRoomNativeFederated, usersUserName: string[], inviter: IUser): Promise { + async inviteUsersToRoom(room: IRoomNativeFederated, matrixUsersUsername: string[], inviter: IUser): Promise { try { const inviterUserId = `@${inviter.username}:${this.serverName}`; await Promise.all( - usersUserName - .filter((username) => { - const isExternalUser = username.includes(':'); - return isExternalUser; - }) - .map(async (username) => { - const alreadyMember = await Subscriptions.findOneByRoomIdAndUsername(room._id, username, { projection: { _id: 1 } }); - if (alreadyMember) { - return; - } + matrixUsersUsername.map(async (username) => { + if (validateFederatedUsername(username)) { + return this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId); + } + + // if inviter is an external user it means we receive the invite from the endpoint + // since we accept from there we can skip accepting here + if (isUserNativeFederated(inviter)) { + this.logger.debug('Inviter is native federated, skip accept invite'); + return; + } + + const result = await this.homeserverServices.invite.inviteUserToRoom( + `@${username}:${this.serverName}`, + room.federation.mrid, + inviterUserId, + ); - await this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId); - }), + return acceptInvite(result.event, username, this.homeserverServices); + }), ); } catch (error) { this.logger.error({ msg: 'Failed to invite an user to Matrix:', err: error }); diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index f61ace906b70b..3e564f9a077ae 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -1,5 +1,5 @@ import { Room } from '@rocket.chat/core-services'; -import type { IUser, UserStatus } from '@rocket.chat/core-typings'; +import { isUserNativeFederated, type IUser } from '@rocket.chat/core-typings'; import type { HomeserverServices, RoomService, @@ -12,6 +12,8 @@ import { Router } from '@rocket.chat/http-router'; import { Rooms, Users } from '@rocket.chat/models'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; +import { createOrUpdateFederatedUser, getUsernameServername } from '../../FederationMatrix'; + const EventBaseSchema = { type: 'object', properties: { @@ -141,11 +143,11 @@ async function runWithBackoff(fn: () => Promise, delaySec = 5) { try { await fn(); } catch (e) { - const delay = delaySec === 625 ? 625 : delaySec ** 2; - console.log(`error occurred, retrying in ${delay}ms`, e); + const delay = Math.max(625, delaySec ** 2); + console.error(`error occurred, retrying in ${delay}ms`, e); setTimeout(() => { - runWithBackoff(fn, delay * 1000); - }, delay); + runWithBackoff(fn, delay); + }, delay * 1000); } } @@ -183,33 +185,14 @@ async function joinRoom({ // need both the sender and the participating user to exist in the room // TODO implement on model - const senderUser = await Users.findOne({ 'federation.mui': inviteEvent.sender }, { projection: { _id: 1 } }); - - let senderUserId = senderUser?._id; + const senderUser = await Users.findOneByUsername(inviteEvent.sender, { projection: { _id: 1 } }); - // create locally - if (!senderUser) { - const createdUser = await Users.insertOne({ - // let the _id auto generate we deal with usernames + const senderUserId = + senderUser?._id || + (await createOrUpdateFederatedUser({ username: inviteEvent.sender, - type: 'user', - status: 'online' as UserStatus, - active: true, - roles: ['user'], - name: inviteEvent.sender, - requirePasswordChange: false, - federated: true, - federation: { - version: 1, - mui: inviteEvent.sender, - origin: matrixRoom.origin, - }, - createdAt: new Date(), - _updatedAt: new Date(), - }); - - senderUserId = createdUser.insertedId; - } + origin: matrixRoom.origin, + })); if (!senderUserId) { throw new Error('Sender user ID not found'); @@ -281,17 +264,56 @@ async function joinRoom({ } await Room.addUserToRoom(internalRoomId, { _id: user._id }, { _id: senderUserId, username: inviteEvent.sender }); - - // TODO is this needed? - // if (isDM) { - // await MatrixBridgedRoom.createOrUpdateByLocalRoomId(internalRoomId, inviteEvent.roomId, matrixRoom.origin); - // } } async function startJoiningRoom(...opts: Parameters) { void runWithBackoff(() => joinRoom(...opts)); } +// This is a special case where inside rocket chat we invite users inside rockechat, so if the sender or the invitee are external iw should throw an error +export const acceptInvite = async ( + inviteEvent: PersistentEventBase, + username: string, + services: HomeserverServices, +) => { + if (!inviteEvent.stateKey) { + throw new Error('join event has missing state key, unable to determine user to join'); + } + + const internalMappedRoom = await Rooms.findOne({ 'federation.mrid': inviteEvent.roomId }); + if (!internalMappedRoom) { + throw new Error('room not found not processing invite'); + } + + const inviter = await Users.findOneByUsername>( + getUsernameServername(inviteEvent.sender, services.config.serverName)[0], + { + projection: { _id: 1, username: 1 }, + }, + ); + + if (!inviter) { + throw new Error('Sender user ID not found'); + } + if (isUserNativeFederated(inviter)) { + throw new Error('Sender user is native federated'); + } + + const user = await Users.findOneByUsername>(username, { + projection: { username: 1, federation: 1, federated: 1 }, + }); + + // we cannot accept invites from users that are external + if (!user) { + throw new Error('User not found'); + } + if (isUserNativeFederated(user)) { + throw new Error('User is native federated'); + } + + await services.room.joinUser(inviteEvent.roomId, inviteEvent.stateKey); +}; + export const getMatrixInviteRoutes = (services: HomeserverServices) => { const { invite, state, room } = services; diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index 7628a520624b4..cdf1354e3fcbb 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -36,7 +36,7 @@ export const edus = async (emitter: Emitter, eduProce } try { - const matrixUser = await Users.findOne({ 'federation.mui': data.user_id }); + const matrixUser = await Users.findOneByUsername(data.user_id); if (!matrixUser) { logger.debug(`No federated user found for Matrix user_id: ${data.user_id}`); return; diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index 9cf3abd0a9539..bbe04a7fd66d5 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -1,5 +1,5 @@ import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; +import { getAllServices, type HomeserverEventSignatures, type HomeserverServices } from '@rocket.chat/federation-sdk'; import { edus } from './edu'; import { member } from './member'; @@ -12,11 +12,12 @@ export function registerEvents( emitter: Emitter, serverName: string, eduProcessTypes: { typing: boolean; presence: boolean }, + services: HomeserverServices = getAllServices(), ) { ping(emitter); message(emitter, serverName); reaction(emitter); - member(emitter); + member(emitter, services); edus(emitter, eduProcessTypes); - room(emitter); + room(emitter, services); } diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index efa6c865aad0d..b165d9cbc958c 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -1,9 +1,10 @@ import { Room } from '@rocket.chat/core-services'; -import { UserStatus } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; +import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/federation-sdk'; import { Logger } from '@rocket.chat/logger'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; + +import { createOrUpdateFederatedUser, getUsernameServername } from '../FederationMatrix'; const logger = new Logger('federation-matrix:member'); @@ -15,7 +16,7 @@ async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver } // state_key is the user affected by the membership change - const affectedUser = await Users.findOne({ 'federation.mui': data.state_key }); + const affectedUser = await Users.findOneByUsername(data.state_key); if (!affectedUser) { logger.error(`No Rocket.Chat user found for bridged user: ${data.state_key}`); return; @@ -28,7 +29,7 @@ async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver logger.info(`User ${affectedUser.username} left room ${room._id} via Matrix federation`); } else { // Kick - find who kicked - const kickerUser = await Users.findOne({ 'federation.mui': data.sender }); + const kickerUser = await Users.findOneByUsername(data.sender); await Room.removeUserFromRoom(room._id, affectedUser, { byUser: kickerUser || { _id: 'matrix.federation', username: 'Matrix User' }, @@ -39,44 +40,39 @@ async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver } } -async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) { +async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership'], services: HomeserverServices) { const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); if (!room) { logger.warn(`No bridged room found for room_id: ${data.room_id}`); return; } - const internalUsername = data.sender; - const localUser = await Users.findOneByUsername(internalUsername); + const [username, serverName, isLocal] = getUsernameServername(data.sender, services.config.serverName); + + // for local users we must to remove the @ and the server domain + const localUser = isLocal && (await Users.findOneByUsername(username)); + if (localUser) { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, localUser._id); + if (subscription) { + return; + } await Room.addUserToRoom(room._id, localUser); return; } - const [, serverName] = data.sender.split(':'); if (!serverName) { throw new Error('Invalid sender format, missing server name'); } - const { insertedId } = await Users.insertOne({ - username: internalUsername, - type: 'user', - status: UserStatus.OFFLINE, - active: true, - roles: ['user'], - name: data.content.displayname || internalUsername, - requirePasswordChange: false, - createdAt: new Date(), - _updatedAt: new Date(), - federated: true, - federation: { - version: 1, - mui: data.sender, - origin: serverName, - }, + const insertedId = await createOrUpdateFederatedUser({ + username: data.state_key as `@${string}:${string}`, + origin: serverName, + name: data.content.displayname || (data.state_key as `@${string}:${string}`), }); const user = await Users.findOneById(insertedId); + if (!user) { console.warn(`User with ID ${insertedId} not found after insertion`); return; @@ -84,7 +80,7 @@ async function membershipJoinAction(data: HomeserverEventSignatures['homeserver. await Room.addUserToRoom(room._id, user); } -export function member(emitter: Emitter) { +export function member(emitter: Emitter, services: HomeserverServices) { emitter.on('homeserver.matrix.membership', async (data) => { try { if (data.content.membership === 'leave') { @@ -92,7 +88,7 @@ export function member(emitter: Emitter) { } if (data.content.membership === 'join') { - return membershipJoinAction(data); + return membershipJoinAction(data, services); } logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index d17a59cae1058..e56cdb40a2a23 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -124,7 +124,7 @@ export function message(emitter: Emitter, serverName: } // at this point we know for sure the user already exists - const user = await Users.findOne({ 'federation.mui': data.sender }); + const user = await Users.findOneByUsername(data.sender); if (!user) { throw new Error(`User not found for sender: ${data.sender}`); } diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index 00deac2ad2419..d5a05e89e5a3f 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -1,9 +1,11 @@ import { Room } from '@rocket.chat/core-services'; import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; +import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/federation-sdk'; import { Rooms, Users } from '@rocket.chat/models'; -export function room(emitter: Emitter) { +import { getUsernameServername } from '../FederationMatrix'; + +export function room(emitter: Emitter, services: HomeserverServices) { emitter.on('homeserver.matrix.room.name', async (data) => { const { room_id: roomId, name, user_id: userId } = data; @@ -12,7 +14,7 @@ export function room(emitter: Emitter) { throw new Error('mapped room not found'); } - const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + const localUserId = await Users.findOneByUsername(userId, { projection: { _id: 1 } }); if (!localUserId) { throw new Error('mapped user not found'); } @@ -28,7 +30,7 @@ export function room(emitter: Emitter) { throw new Error('mapped room not found'); } - const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + const localUserId = await Users.findOneByUsername(userId, { projection: { _id: 1 } }); if (!localUserId) { throw new Error('mapped user not found'); } @@ -44,12 +46,19 @@ export function room(emitter: Emitter) { throw new Error('mapped room not found'); } - const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + const [allegedUsernameLocal, , allegedUserLocalIsLocal] = getUsernameServername(userId, services.config.serverName); + const localUserId = allegedUserLocalIsLocal && (await Users.findOneByUsername(allegedUsernameLocal, { projection: { _id: 1 } })); if (!localUserId) { throw new Error('mapped user not found'); } - const localSenderId = await Users.findOne({ 'federation.mui': senderId }, { projection: { _id: 1 } }); + const [senderUsername, , senderIsLocal] = getUsernameServername(senderId, services.config.serverName); + + if (senderIsLocal) { + return; + } + + const localSenderId = await Users.findOneByUsername(senderUsername, { projection: { _id: 1 } }); if (!localSenderId) { throw new Error('mapped user not found'); } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index bd76e342b7a5d..f725b2738cf92 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -265,9 +265,10 @@ export const isUserFederated = (user: Partial | Partial export interface IUserNativeFederated extends IUser { federated: true; + username: `@${string}:${string}`; federation: { version: number; - mui: string; + mui: `@${string}:${string}`; origin: string; }; }