diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index c5f28d1bdea10..9052c7e39e019 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -22,9 +22,10 @@ jobs: runs-on: ubuntu-24.04 name: Test Storybook - + steps: - uses: actions/checkout@v4 + - name: Setup NodeJS uses: ./.github/actions/setup-node with: @@ -54,4 +55,3 @@ jobs: flags: unit verbose: true token: ${{ secrets.CODECOV_TOKEN }} - \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8741b33f36c35..81bca21648cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ registration.yaml storybook-static development/tempo-data/ + +.env diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index a86f16180954a..a092ce877ba10 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -96,6 +96,6 @@ export async function saveRoomName( await Message.saveSystemMessage('r', rid, displayName, user); } - await callbacks.run('afterRoomNameChange', { room, name: displayName, oldName: room.name, userId: user._id }); + await callbacks.run('afterRoomNameChange', { room, name: displayName, oldName: room.name, user }); return displayName; } diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts index a59f2ba82fba5..0a8a83be49c1e 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; -export const saveRoomTopic = async function ( +export const saveRoomTopic = async ( rid: string, roomTopic: string | undefined, user: { @@ -13,7 +13,7 @@ export const saveRoomTopic = async function ( _id: string; }, sendMessage = true, -) { +) => { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveRoomTopic', @@ -28,6 +28,6 @@ export const saveRoomTopic = async function ( if (update && sendMessage) { await Message.saveSystemMessage('room_changed_topic', rid, roomTopic || '', user); } - await callbacks.run('afterRoomTopicChange', { rid, topic: roomTopic }); + await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user }); return update; }; diff --git a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts index 4e2f9a8667361..86b2b30b699df 100644 --- a/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts +++ b/apps/meteor/app/discussion/server/hooks/propagateDiscussionMetadata.ts @@ -48,7 +48,7 @@ callbacks.add( callbacks.add( 'afterDeleteMessage', - async (message, { _id, prid }) => { + async (message, { room: { _id, prid } }) => { if (prid) { const room = await Rooms.findOneById(_id, { projection: { diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 2b477c694c5d0..70e0746c18cd5 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -1,7 +1,7 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import { type IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -19,7 +19,7 @@ import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib * Caution - It does not validates if the user has permission to join room */ -export const addUserToRoom = async function ( +export const addUserToRoom = async ( rid: string, user: Pick | string, inviter?: Pick, @@ -32,7 +32,7 @@ export const addUserToRoom = async function ( skipAlertSound?: boolean; createAsHidden?: boolean; } = {}, -): Promise { +): Promise => { const now = new Date(); const room = await Rooms.findOneById(rid); @@ -57,7 +57,7 @@ export const addUserToRoom = async function ( } try { - await beforeAddUserToRoom.run({ user: userToBeAdded, inviter }, room); + await beforeAddUserToRoom.run({ user: userToBeAdded, inviter: (inviter && (await Users.findOneById(inviter._id))) || undefined }, room); } catch (error) { throw new Meteor.Error((error as any)?.message); } diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index f77ee1f55901b..2e0f34c0e1f31 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -42,10 +42,11 @@ const getName = (members: IUser[]): string => members.map(({ username }) => user export async function createDirectRoom( members: IUser[] | string[], - roomExtraData = {}, + roomExtraData: Partial = {}, options: { creator?: string; subscriptionExtra?: ISubscriptionExtraData; + federatedRoomId?: string; }, ): Promise { const maxUsers = settings.get('DirectMesssage_maxUsers') || 1; @@ -59,17 +60,17 @@ export async function createDirectRoom( ); } - await callbacks.run('beforeCreateDirectRoom', members); - const membersUsernames: string[] = members .map((member) => { if (typeof member === 'string') { - return member.replace('@', ''); + return member; } return member.username; }) .filter(isTruthy); + 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(); @@ -179,7 +180,11 @@ export async function createDirectRoom( if (isNewRoom) { const insertedRoom = await Rooms.findOneById(rid); - await callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator }); + await callbacks.run('afterCreateDirectRoom', insertedRoom, { + members: roomMembers, + creatorId: options?.creator, + mrid: options?.federatedRoomId, + }); void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, insertedRoom); } diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 0ef03ee7b85b1..af078945ef0be 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -68,6 +68,7 @@ async function createUsersSubscriptions({ const membersCursor = Users.findUsersByUsernames(members); + // TODO: Check re new federation-service - should we add them here or keep on createRoom inside of homeserver?! for await (const member of membersCursor) { try { await beforeAddUserToRoom.run({ user: member, inviter: owner }, room); @@ -131,7 +132,25 @@ export const createRoom = async ( rid: string; } > => { - const { teamId, ...extraData } = roomExtraData || ({} as IRoom); + const { teamId, ...optionalExtraData } = roomExtraData || ({} as IRoom); + + const hasFederatedMembers = members.some((member) => { + if (typeof member === 'string') { + return member.includes(':') && member.includes('@'); + } + return member.username?.includes(':') && member.username?.includes('@'); + }); + + const extraData = { + ...optionalExtraData, + ...((hasFederatedMembers || optionalExtraData.federated) && { + federated: true, + federation: { + version: 1, + // TODO we should be able to provide all values from here, currently we update on callback afterCreateRoom + }, + }), + }; await prepareCreateRoomCallback.run({ type, @@ -265,6 +284,7 @@ export const createRoom = async ( callbacks.runAsync('afterCreatePrivateGroup', owner, room); } callbacks.runAsync('afterCreateRoom', owner, room); + if (shouldBeHandledByFederation) { callbacks.runAsync('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members, options }); } diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 391ba936241f5..7e1dcddd921af 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -93,7 +93,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise, - user: IUser, - message: IMessage, - reaction: string, - userAlreadyReacted?: boolean, -) { +export async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction: string, userAlreadyReacted?: boolean) { await Message.beforeReacted(message, room); if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { @@ -71,7 +65,7 @@ export async function setReaction( await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } - void callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact: false, oldMessage }); + void callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact: false, oldMessage, room }); isReacted = false; } else { @@ -89,7 +83,7 @@ export async function setReaction( await Rooms.setReactionsInLastMessage(room._id, message.reactions); } - void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true }); + void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true, room }); isReacted = true; } @@ -139,9 +133,7 @@ export async function executeSetReaction( return; } - const room = await Rooms.findOneById< - Pick - >(message.rid, { projection: { _id: 1, ro: 1, muted: 1, reactWhenReadOnly: 1, lastMessage: 1, t: 1, prid: 1, federated: 1 } }); + const room = await Rooms.findOneById(message.rid); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx index 2b1f7415ab452..59f864be88384 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx @@ -166,7 +166,7 @@ describe('useReloadOnError', () => { ); // Mock fetch to return first, then second - (global.fetch as jest.Mock) = jest.fn().mockResolvedValueOnce(firstReply).mockResolvedValueOnce(secondReply); + (global.fetch as unknown as jest.Mock) = jest.fn().mockResolvedValueOnce(firstReply).mockResolvedValueOnce(secondReply); const { result } = renderHook(() => useReloadOnError('/sampleurl?token=old', 'audio')); const media = makeMediaEl(); @@ -217,7 +217,7 @@ describe('useReloadOnError', () => { it('ignores initial play when expiry is unknown', async () => { // no fetch expected on first play because expiresAt is not known yet - global.fetch = jest.fn(); + (global.fetch as unknown as jest.Mock) = jest.fn(); const { result } = renderHook(() => useReloadOnError('/foo', 'audio')); const media = makeMediaEl(); diff --git a/apps/meteor/client/hooks/useIsFederationEnabled.ts b/apps/meteor/client/hooks/useIsFederationEnabled.ts index ee7cdcec0e725..0cbbb9438dcea 100644 --- a/apps/meteor/client/hooks/useIsFederationEnabled.ts +++ b/apps/meteor/client/hooks/useIsFederationEnabled.ts @@ -1,6 +1,8 @@ import { useSetting } from '@rocket.chat/ui-contexts'; export const useIsFederationEnabled = () => { - const federationMatrixEnabled = useSetting('Federation_Matrix_enabled', false) === true; - return federationMatrixEnabled; + const matrixFederationEnabled = useSetting('Federation_Matrix_enabled', false); + const serviceFederationEnabled = useSetting('Federation_Service_Enabled', false); + const federationEnabled = matrixFederationEnabled || serviceFederationEnabled; + return federationEnabled; }; diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts index 69732d0cb859f..ac97caba1395a 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts @@ -1,5 +1,6 @@ import { Base64 } from '@rocket.chat/base64'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, AtLeast } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, AtLeast, EncryptedMessageContent } from '@rocket.chat/core-typings'; +import { isEncryptedMessageContent } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { Optional } from '@tanstack/react-query'; import EJSON from 'ejson'; @@ -670,11 +671,9 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - async decryptContent(data: T) { - if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { - const content = await this.decrypt(data.content.ciphertext); - Object.assign(data, content); - } + async decryptContent(data: T) { + const content = await this.decrypt(data.content.ciphertext); + Object.assign(data, content); return data; } @@ -693,7 +692,7 @@ export class E2ERoom extends Emitter { } } - message = await this.decryptContent(message); + message = isEncryptedMessageContent(message) ? await this.decryptContent(message) : message; return { ...message, diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts index 268cdfa4c8aeb..cd42765297d49 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts @@ -2,7 +2,7 @@ import QueryString from 'querystring'; import URL from 'url'; import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, MessageAttachment } from '@rocket.chat/core-typings'; -import { isE2EEMessage } from '@rocket.chat/core-typings'; +import { isE2EEMessage, isEncryptedMessageContent } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { imperativeModal } from '@rocket.chat/ui-client'; import EJSON from 'ejson'; @@ -664,7 +664,7 @@ class E2E extends Emitter { } async decryptFileContent(file: IUploadWithUser): Promise { - if (!file.rid) { + if (!file.rid || !isEncryptedMessageContent(file)) { return file; } diff --git a/apps/meteor/ee/server/api/federation.ts b/apps/meteor/ee/server/api/federation.ts new file mode 100644 index 0000000000000..278282ed0ec1b --- /dev/null +++ b/apps/meteor/ee/server/api/federation.ts @@ -0,0 +1,24 @@ +import type { IFederationMatrixService } from '@rocket.chat/core-services'; +import { Logger } from '@rocket.chat/logger'; +import type express from 'express'; +import { WebApp } from 'meteor/webapp'; + +import { isRunningMs } from '../../../server/lib/isRunningMs'; + +const logger = new Logger('FederationRoutes'); + +export async function registerFederationRoutes(federationService: IFederationMatrixService): Promise { + if (isRunningMs()) { + return; + } + + try { + const routes = federationService.getAllRoutes(); + (WebApp.rawConnectHandlers as unknown as ReturnType).use(routes.matrix.router).use(routes.wellKnown.router); + + logger.log('[Federation] Registered federation routes'); + } catch (error) { + logger.error('[Federation] Failed to register routes:', error); + throw error; + } +} diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts new file mode 100644 index 0000000000000..67ad46bcd15e9 --- /dev/null +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -0,0 +1,235 @@ +import { FederationMatrix } from '@rocket.chat/core-services'; +import { isEditedMessage, isUserNativeFederated, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; +import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback'; +import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; +import { beforeChangeRoomRole } from '../../../../lib/callbacks/beforeChangeRoomRole'; +import { FederationActions } from '../../../../server/services/room/hooks/BeforeFederationActions'; + +// callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); + +// TODO: move this to the hooks folder +callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members, options }) => { + if (FederationActions.shouldPerformFederationAction(room)) { + const federatedRoomId = options?.federatedRoomId; + + if (!federatedRoomId) { + // if room exists, we don't want to create it again + // adds bridge record + await FederationMatrix.createRoom(room, owner, members); + } else { + // matrix room was already created and passed + const fromServer = federatedRoomId.split(':')[1]; + + await Rooms.setAsFederated(room._id, { + mrid: federatedRoomId, + origin: fromServer, + }); + } + } +}); + +callbacks.add( + 'afterSaveMessage', + async (message, { room, user }) => { + if (!FederationActions.shouldPerformFederationAction(room)) { + return; + } + + try { + // TODO: Check if message already exists in the database, if it does, don't send it to the federation to avoid loops + // If message is federated, it will save external_message_id like into the message object + // if this prop exists here it should not be sent to the federation to avoid loops + if (!message.federation?.eventId) { + await FederationMatrix.sendMessage(message, room, user); + } + } catch (error) { + // Log the error but don't prevent the message from being sent locally + console.error('[sendMessage] Failed to send message to Native Federation:', error); + } + }, + callbacks.priority.HIGH, + 'native-federation-after-room-message-sent', +); + +callbacks.add( + 'afterDeleteMessage', + async (message: IMessage, { room, user }) => { + if (!message.federation?.eventId) { + return; + } + + // removing messages from external users is not allowed + // TODO should we make it work for external users? + if (user.federated) { + return; + } + + if (!isUserNativeFederated(user)) { + return; + } + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.deleteMessage(room.federation.mrid, message, user.federation.mui); + } + }, + callbacks.priority.MEDIUM, + '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) { + return; + } + + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.inviteUsersToRoom(room, [user.username], inviter); + } + }, + callbacks.priority.MEDIUM, + 'native-federation-on-before-add-users-to-room', +); + +callbacks.add( + 'afterSetReaction', + async (message: IMessage, params): Promise => { + // Don't federate reactions that came from Matrix + if (params.user.username?.includes(':')) { + return; + } + if (FederationActions.shouldPerformFederationAction(params.room)) { + await FederationMatrix.sendReaction(message._id, params.reaction, params.user); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-set-reaction', +); + +callbacks.add( + 'afterUnsetReaction', + async (_message: IMessage, params): Promise => { + // Don't federate reactions that came from Matrix + if (params.user.username?.includes(':')) { + return; + } + if (FederationActions.shouldPerformFederationAction(params.room)) { + await FederationMatrix.removeReaction(params.oldMessage._id, params.reaction, params.user, params.oldMessage); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-unset-reaction', +); + +afterLeaveRoomCallback.add( + async (user: IUser, room: IRoom): Promise => { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.leaveRoom(room._id, user); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-leave-room', +); + +afterRemoveFromRoomCallback.add( + async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise => { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.kickUser(room, data.removedUser, data.userWhoRemoved); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-remove-from-room', +); + +callbacks.add( + 'afterRoomNameChange', + async ({ room, name, user }) => { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.updateRoomName(room._id, name, user); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-name-changed', +); + +callbacks.add( + 'afterRoomTopicChange', + async (_, { room, topic, user }) => { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.updateRoomTopic(room, topic, user); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-topic-changed', +); + +callbacks.add( + 'afterSaveMessage', + async (message: IMessage, { room }) => { + if (FederationActions.shouldPerformFederationAction(room)) { + if (!isEditedMessage(message)) { + return; + } + + await FederationMatrix.updateMessage(room, message); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-message-updated', +); + +beforeChangeRoomRole.add( + async (params: { fromUserId: string; userId: string; room: IRoom; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { + if (FederationActions.shouldPerformFederationAction(params.room)) { + await FederationMatrix.addUserRoleRoomScoped(params.room, params.fromUserId, params.userId, params.role); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-before-change-room-role', +); + +callbacks.add( + 'beforeCreateDirectRoom', + async (members, room): Promise => { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.ensureFederatedUsersExistLocally(members); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-before-create-direct-room', +); + +callbacks.add( + 'afterCreateDirectRoom', + async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id']; mrid?: string }): Promise => { + if (params.mrid) { + await Rooms.setAsFederated(room._id, { + mrid: params.mrid, + origin: params.mrid.split(':').pop()!, + }); + return; + } + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-create-direct-room', +); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index c9340d1f440a4..e3604bbb36141 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -13,6 +13,7 @@ import './configuration/index'; import './local-services/ldap/service'; import './methods/getReadReceipts'; import './patches'; +import './hooks/federation'; export * from './apps/startup'; export { registerEEBroker } from './startup'; diff --git a/apps/meteor/ee/server/lib/engagementDashboard/messages.ts b/apps/meteor/ee/server/lib/engagementDashboard/messages.ts index e3d99ac3039a5..55bcae0da3aeb 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/messages.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/messages.ts @@ -19,7 +19,7 @@ export const handleMessagesSent = async (message: IMessage, { room }: { room?: I return message; }; -export const handleMessagesDeleted = async (message: IMessage, room?: IRoom): Promise => { +export const handleMessagesDeleted = async (message: IMessage, { room }: { room: IRoom }): Promise => { const roomTypesToShow = roomCoordinator.getTypesToShowOnDashboard(); if (!room || !roomTypesToShow.includes(room.t)) { return message; diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts new file mode 100644 index 0000000000000..47636b6cc2b12 --- /dev/null +++ b/apps/meteor/ee/server/startup/federation.ts @@ -0,0 +1,135 @@ +import { api } from '@rocket.chat/core-services'; +import { FederationMatrix } from '@rocket.chat/federation-matrix'; +import { InstanceStatus } from '@rocket.chat/instance-status'; +import { License } from '@rocket.chat/license'; +import { Logger } from '@rocket.chat/logger'; + +import { settings } from '../../../app/settings/server'; +import { StreamerCentral } from '../../../server/modules/streamer/streamer.module'; +import { registerFederationRoutes } from '../api/federation'; + +const logger = new Logger('Federation'); + +// TODO: should validate if the domain is resolving to us or not correctly +// should use homeserver.getFinalSomethingSomething and validate final Host header to have siteUrl +// this is a minimum sanity check to avoid full urls instead of the expected domain part +function validateDomain(domain: string): boolean { + const value = domain.trim(); + + if (!value) { + logger.error('The Federation domain is not set'); + return false; + } + + if (value.toLowerCase() !== value) { + logger.error(`The Federation domain "${value}" cannot have uppercase letters`); + return false; + } + + try { + const valid = new URL(`https://${value}`).hostname === value; + + if (!valid) { + throw new Error(); + } + } catch { + logger.error(`The configured Federation domain "${value}" is not valid`); + return false; + } + + return true; +} + +export const startFederationService = async (): Promise => { + let federationMatrixService: FederationMatrix | undefined; + + const shouldStartService = (): boolean => { + const hasLicense = License.hasModule('federation'); + const isEnabled = settings.get('Federation_Service_Enabled') === true; + const domain = settings.get('Federation_Service_Domain'); + const hasDomain = validateDomain(domain); + return hasLicense && isEnabled && hasDomain; + }; + + const startService = async (): Promise => { + if (federationMatrixService) { + logger.debug('Federation-matrix service already started... skipping'); + return; + } + + logger.debug('Starting federation-matrix service'); + federationMatrixService = await FederationMatrix.create(InstanceStatus.id()); + + StreamerCentral.on('broadcast', (name, eventName, args) => { + if (!federationMatrixService) { + return; + } + if (name === 'notify-room' && eventName.endsWith('user-activity')) { + const [rid] = eventName.split('/'); + const [user, activity] = args; + void federationMatrixService.notifyUserTyping(rid, user, activity.includes('user-typing')); + } + }); + + try { + api.registerService(federationMatrixService); + await registerFederationRoutes(federationMatrixService); + } catch (error) { + logger.error('Failed to start federation-matrix service:', error); + } + }; + + const stopService = async (): Promise => { + if (!federationMatrixService) { + logger.debug('Federation-matrix service not registered... skipping'); + return; + } + + logger.debug('Stopping federation-matrix service'); + + // TODO: Unregister routes + // await unregisterFederationRoutes(federationMatrixService); + + await api.destroyService(federationMatrixService); + federationMatrixService = undefined; + }; + + if (shouldStartService()) { + await startService(); + } + + void License.onLicense('federation', async () => { + logger.debug('Federation license became available'); + if (shouldStartService()) { + await startService(); + } + }); + + License.onInvalidateLicense(async () => { + logger.debug('License invalidated, checking federation module'); + if (!shouldStartService()) { + await stopService(); + } + }); + + settings.watch('Federation_Service_Enabled', async (enabled) => { + logger.debug('Federation_Service_Enabled setting changed:', enabled); + if (shouldStartService()) { + await startService(); + } else { + await stopService(); + } + }); + + settings.watch('Federation_Service_Domain', async (domain) => { + logger.debug('Federation_Service_Domain setting changed:', domain); + if (shouldStartService()) { + if (domain.toLowerCase() !== federationMatrixService?.getServerName().toLowerCase()) { + await stopService(); + } + await startService(); + } else { + await stopService(); + } + }); +}; diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 2a8410fe37685..7fe2240f2342e 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -41,14 +41,14 @@ interface EventLikeCallbackSignatures { 'afterCreateChannel': (owner: IUser, room: IRoom) => void; 'afterCreatePrivateGroup': (owner: IUser, room: IRoom) => void; 'afterDeactivateUser': (user: IUser) => void; - 'afterDeleteMessage': (message: IMessage, room: IRoom) => void; + 'afterDeleteMessage': (message: IMessage, params: { room: IRoom; user: IUser }) => void; 'workspaceLicenseChanged': (license: string) => void; 'workspaceLicenseRemoved': () => void; 'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void; 'beforeReadMessages': (rid: IRoom['_id'], uid: IUser['_id']) => void; 'afterDeleteUser': (user: IUser) => void; 'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void; - 'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; userId: IUser['_id'] }) => void; + 'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser }) => void; 'afterSaveMessage': (message: IMessage, params: { room: IRoom; user: IUser; roomUpdater?: Updater }) => void; 'afterOmnichannelSaveMessage': (message: IMessage, constant: { room: IOmnichannelRoom; roomUpdater: Updater }) => void; 'livechat.removeAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void; @@ -64,7 +64,7 @@ interface EventLikeCallbackSignatures { user: AtLeast; inviter: AtLeast; }) => void; - 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void; + 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id']; mrid?: string }) => void; 'beforeDeleteRoom': (params: IRoom) => void; 'beforeJoinDefaultChannels': (user: IUser) => void; 'beforeCreateChannel': (owner: IUser, room: IRoom) => void; @@ -78,12 +78,12 @@ interface EventLikeCallbackSignatures { options?: ICreateRoomOptions; }, ) => void; - 'beforeCreateDirectRoom': (members: IUser[]) => void; + 'beforeCreateDirectRoom': (members: string[], room: IRoom) => void; 'federation.beforeCreateDirectMessage': (members: IUser[]) => void; - 'afterSetReaction': (message: IMessage, { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean }) => void; + 'afterSetReaction': (message: IMessage, params: { user: IUser; reaction: string; shouldReact: boolean; room: IRoom }) => void; 'afterUnsetReaction': ( message: IMessage, - { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean; oldMessage: 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; @@ -205,6 +205,7 @@ type ChainedCallbackSignatures = { 'roomAvatarChanged': (room: IRoom) => void; 'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise; 'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void; + 'afterRoomTopicChange': (params: undefined, { room, topic, user }: { room: IRoom; topic: string; user: IUser }) => void; }; export type Hook = @@ -212,7 +213,6 @@ export type Hook = | keyof ChainedCallbackSignatures | 'afterProcessOAuthUser' | 'afterRoomArchived' - | 'afterRoomTopicChange' | 'afterSaveUser' | 'afterValidateNewOAuthUser' | 'beforeActivateUser' diff --git a/apps/meteor/lib/callbacks/beforeAddUserToRoom.ts b/apps/meteor/lib/callbacks/beforeAddUserToRoom.ts index ce93a4ee59af6..c410152057dea 100644 --- a/apps/meteor/lib/callbacks/beforeAddUserToRoom.ts +++ b/apps/meteor/lib/callbacks/beforeAddUserToRoom.ts @@ -2,5 +2,4 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Callbacks } from './callbacksBase'; -export const beforeAddUserToRoom = - Callbacks.create<(args: { user: IUser; inviter?: Pick }, room: IRoom) => void>('beforeAddUserToRoom'); +export const beforeAddUserToRoom = Callbacks.create<(args: { user: IUser; inviter?: IUser }, room: IRoom) => void>('beforeAddUserToRoom'); diff --git a/apps/meteor/lib/callbacks/beforeChangeRoomRole.ts b/apps/meteor/lib/callbacks/beforeChangeRoomRole.ts index 036e557f94a4a..f83d0834d5fc6 100644 --- a/apps/meteor/lib/callbacks/beforeChangeRoomRole.ts +++ b/apps/meteor/lib/callbacks/beforeChangeRoomRole.ts @@ -1,6 +1,8 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + import { Callbacks } from './callbacksBase'; export const beforeChangeRoomRole = - Callbacks.create<(args: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => void>( + Callbacks.create<(args: { fromUserId: string; userId: string; room: IRoom; role: 'moderator' | 'owner' | 'leader' | 'user' }) => void>( 'beforeChangeRoomRole', ); diff --git a/apps/meteor/lib/callbacks/callbacksBase.ts b/apps/meteor/lib/callbacks/callbacksBase.ts index 405cc5da80e66..ff7d03804bb06 100644 --- a/apps/meteor/lib/callbacks/callbacksBase.ts +++ b/apps/meteor/lib/callbacks/callbacksBase.ts @@ -123,27 +123,34 @@ export class Callbacks< callback: TEventLikeCallbackSignatures[Hook], priority?: CallbackPriority, id?: string, - ): void; + ): () => void; add( hook: Hook, callback: TChainedCallbackSignatures[Hook], priority?: CallbackPriority, id?: string, - ): void; + ): () => void; add( hook: THook, callback: (item: TItem, constant?: TConstant) => TNextItem, priority?: CallbackPriority, id?: string, - ): void; + ): () => void; - add(hook: THook, callback: (item: unknown, constant?: unknown) => unknown, priority = this.priority.MEDIUM, id = Random.id()): void { + add( + hook: THook, + callback: (item: unknown, constant?: unknown) => unknown, + priority = this.priority.MEDIUM, + id = Random.id(), + ): () => void { const callbacks = this.getCallbacks(hook); if (callbacks.some((cb) => cb.id === id)) { - return; + return () => { + this.remove(hook, id); + }; } callbacks.push( @@ -157,6 +164,10 @@ export class Callbacks< callbacks.sort(compareByRanking((callback: Callback): number => callback.priority ?? this.priority.MEDIUM)); this.setCallbacks(hook, callbacks); + + return () => { + this.remove(hook, id); + }; } /** diff --git a/apps/meteor/lib/publishFields.ts b/apps/meteor/lib/publishFields.ts index d3974bebf8af5..1caa84d6358a3 100644 --- a/apps/meteor/lib/publishFields.ts +++ b/apps/meteor/lib/publishFields.ts @@ -113,6 +113,7 @@ export const roomFields = { // Federation fields federated: 1, + federation: 1, // fields used by DMs usernames: 1, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 116806ccda552..7687c384abfbe 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -225,6 +225,7 @@ "@babel/runtime": "~7.26.10", "@bugsnag/js": "~7.20.2", "@bugsnag/plugin-react": "~7.19.0", + "@datastructures-js/priority-queue": "^6.3.3", "@google-cloud/storage": "^7.15.0", "@kaciras/deasync": "^1.1.0", "@nivo/bar": "0.88.0", @@ -232,6 +233,7 @@ "@nivo/heatmap": "0.88.0", "@nivo/line": "0.88.0", "@nivo/pie": "0.88.0", + "@noble/ed25519": "^3.0.0", "@node-oauth/oauth2-server": "5.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.54.2", @@ -252,6 +254,8 @@ "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", + "@rocket.chat/federation-matrix": "workspace:^", + "@rocket.chat/federation-service": "workspace:^", "@rocket.chat/freeswitch": "workspace:^", "@rocket.chat/fuselage": "^0.66.4", "@rocket.chat/fuselage-forms": "^0.1.0", @@ -434,6 +438,7 @@ "react-keyed-flatten-children": "^3.0.2", "react-stately": "~3.17.0", "react-virtuoso": "^4.12.0", + "reflect-metadata": "^0.2.2", "sanitize-html": "^2.14.0", "semver": "^7.6.3", "sharp": "^0.33.5", @@ -448,6 +453,8 @@ "swiper": "patch:swiper@npm%3A11.1.14#~/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch", "textarea-caret": "^3.1.0", "tinykeys": "^1.4.0", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", "twilio": "^5.4.2", "twit": "^2.2.11", "typia": "~9.7.0", diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index ca0b86d1a5256..3545fdebe8adb 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -199,6 +199,9 @@ export class LDAPManager { ...(homeServer && { username: `${username}:${homeServer}`, federated: true, + federation: { + version: 1, + }, }), }; diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index ad9cae2d7a236..c0cd23b53e4a0 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -11,7 +11,7 @@ import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notify import { settings } from '../../app/settings/server'; import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole'; import { syncRoomRolePriorityForUserAndRoom } from '../lib/roles/syncRoomRolePriority'; -import { isFederationEnabled, isFederationReady, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; +import { isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -39,7 +39,7 @@ export const addRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom['_id }); } - if (isFederated && (!isFederationEnabled() || !isFederationReady())) { + if (isFederated && !isFederationEnabled()) { throw new FederationMatrixInvalidConfigurationError('unable to change room owners'); } @@ -65,7 +65,7 @@ export const addRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom['_id }); } - await beforeChangeRoomRole.run({ fromUserId, userId, roomId: rid, role: 'moderator' }); + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'moderator' }); const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'moderator'); await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.concat(['moderator']) || ['moderator']); diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index 5dd64f9785cba..36d65c6796803 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -11,7 +11,7 @@ import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notify import { settings } from '../../app/settings/server'; import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole'; import { syncRoomRolePriorityForUserAndRoom } from '../lib/roles/syncRoomRolePriority'; -import { isFederationReady, isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; +import { isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -39,7 +39,7 @@ export const addRoomOwner = async (fromUserId: IUser['_id'], rid: IRoom['_id'], }); } - if (isFederated && (!isFederationEnabled() || !isFederationReady())) { + if (isFederated && !isFederationEnabled()) { throw new FederationMatrixInvalidConfigurationError('unable to change room owners'); } @@ -65,7 +65,7 @@ export const addRoomOwner = async (fromUserId: IUser['_id'], rid: IRoom['_id'], }); } - await beforeChangeRoomRole.run({ fromUserId, userId, roomId: rid, role: 'owner' }); + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'owner' }); const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'owner'); await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.concat(['owner']) || ['owner']); diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts index bbd6b486a54eb..ac9a9479a9bce 100644 --- a/apps/meteor/server/methods/createDirectMessage.ts +++ b/apps/meteor/server/methods/createDirectMessage.ts @@ -40,7 +40,9 @@ export async function createDirectMessage( } const users = await Promise.all(usernames.filter((username) => username !== me.username)); + const options: Exclude = { creator: me._id }; const roomUsers = excludeSelf ? users : [me, ...users]; + const federated = false; // allow self-DMs if (roomUsers.length === 1 && roomUsers[0] !== undefined && typeof roomUsers[0] !== 'string' && roomUsers[0]._id !== me._id) { @@ -69,7 +71,6 @@ export async function createDirectMessage( }); } - const options: Exclude = { creator: me._id }; if (excludeSelf && (await hasPermissionAsync(userId, 'view-room-administration'))) { options.subscriptionExtra = { open: true }; } @@ -82,7 +83,18 @@ export async function createDirectMessage( _id: rid, inserted, ...room - } = await createRoom<'d'>('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options); + } = await createRoom<'d'>( + 'd', + undefined, + undefined, + roomUsers as IUser[], + false, + undefined, + { + federated, + }, + options, + ); return { // @ts-expect-error - room type is already defined in the `createRoom` return type diff --git a/apps/meteor/server/methods/removeRoomModerator.ts b/apps/meteor/server/methods/removeRoomModerator.ts index e7580012dd4be..db7808d592d7c 100644 --- a/apps/meteor/server/methods/removeRoomModerator.ts +++ b/apps/meteor/server/methods/removeRoomModerator.ts @@ -58,7 +58,7 @@ export const removeRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom[' }); } - await beforeChangeRoomRole.run({ fromUserId, userId, roomId: rid, role: 'user' }); + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'user' }); const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'moderator'); await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.filter((r) => r !== 'moderator') || []); diff --git a/apps/meteor/server/methods/removeRoomOwner.ts b/apps/meteor/server/methods/removeRoomOwner.ts index 942a7fba16d3e..f39d81ba31f0d 100644 --- a/apps/meteor/server/methods/removeRoomOwner.ts +++ b/apps/meteor/server/methods/removeRoomOwner.ts @@ -64,7 +64,7 @@ export const removeRoomOwner = async (fromUserId: string, rid: string, userId: s }); } - await beforeChangeRoomRole.run({ fromUserId, userId, roomId: rid, role: 'user' }); + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'user' }); const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'owner'); await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.filter((r) => r !== 'owner') || []); diff --git a/apps/meteor/server/models.ts b/apps/meteor/server/models.ts index 5c383a2d9f880..c1cd1be02a649 100644 --- a/apps/meteor/server/models.ts +++ b/apps/meteor/server/models.ts @@ -41,8 +41,6 @@ import { LivechatTriggerRaw, LivechatVisitorsRaw, LoginServiceConfigurationRaw, - MatrixBridgedRoomRaw, - MatrixBridgedUserRaw, MediaCallsRaw, MediaCallChannelsRaw, MediaCallNegotiationsRaw, @@ -135,8 +133,6 @@ registerModel('ILivechatPriorityModel', new LivechatPriorityRaw(db)); registerModel('ILivechatTriggerModel', new LivechatTriggerRaw(db)); registerModel('ILivechatVisitorsModel', new LivechatVisitorsRaw(db)); registerModel('ILoginServiceConfigurationModel', new LoginServiceConfigurationRaw(db)); -registerModel('IMatrixBridgedRoomModel', new MatrixBridgedRoomRaw(db)); -registerModel('IMatrixBridgedUserModel', new MatrixBridgedUserRaw(db)); registerModel('IMediaCallsModel', new MediaCallsRaw(db)); registerModel('IMediaCallChannelsModel', new MediaCallChannelsRaw(db)); registerModel('IMediaCallNegotiationsModel', new MediaCallNegotiationsRaw(db)); diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index c37b22e0b1aef..017108bdec0be 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -185,6 +185,10 @@ export class ListenersModule { }); }); + service.onEvent('user.activity', ({ isTyping, roomId, user }) => { + notifications.notifyRoom(roomId, 'user-activity', user, isTyping ? ['user-typing'] : []); + }); + service.onEvent('watch.messages', async ({ message }) => { if (!message.rid) { return; diff --git a/apps/meteor/server/services/federation/Settings.ts b/apps/meteor/server/services/federation/Settings.ts new file mode 100644 index 0000000000000..36305229fc6be --- /dev/null +++ b/apps/meteor/server/services/federation/Settings.ts @@ -0,0 +1,158 @@ +import crypto from 'crypto'; + +import { v4 as uuidv4 } from 'uuid'; + +import { settings, settingsRegistry } from '../../../app/settings/server'; + +export const addMatrixBridgeFederationSettings = async (): Promise => { + await settingsRegistry.add('Federation_Matrix_enabled', false, { + readonly: true, + type: 'boolean', + i18nLabel: 'Federation_Matrix_enabled', + i18nDescription: 'Federation_Matrix_enabled_desc', + alert: 'Old_Federation_Alert', + public: true, + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_serve_well_known', true, { + readonly: true, + type: 'boolean', + i18nLabel: 'Federation_Matrix_serve_well_known', + alert: 'Federation_Matrix_serve_well_known_Alert', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_enable_ephemeral_events', false, { + readonly: true, + type: 'boolean', + i18nLabel: 'Federation_Matrix_enable_ephemeral_events', + i18nDescription: 'Federation_Matrix_enable_ephemeral_events_desc', + alert: 'Federation_Matrix_enable_ephemeral_events_Alert', + public: true, + group: 'Federation', + section: 'Matrix Bridge', + }); + + const uniqueId = settings.get('uniqueID') || uuidv4().slice(0, 15).replace(new RegExp('-', 'g'), '_'); + const homeserverToken = crypto.createHash('sha256').update(`hs_${uniqueId}`).digest('hex'); + const applicationServiceToken = crypto.createHash('sha256').update(`as_${uniqueId}`).digest('hex'); + + const siteUrl = settings.get('Site_Url'); + + await settingsRegistry.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_id', + i18nDescription: 'Federation_Matrix_id_desc', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_hs_token', homeserverToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_hs_token', + i18nDescription: 'Federation_Matrix_hs_token_desc', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_as_token', applicationServiceToken, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_as_token', + i18nDescription: 'Federation_Matrix_as_token_desc', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_homeserver_url', 'http://localhost:8008', { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_url', + i18nDescription: 'Federation_Matrix_homeserver_url_desc', + alert: 'Federation_Matrix_homeserver_url_alert', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_homeserver_domain', siteUrl, { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_homeserver_domain', + i18nDescription: 'Federation_Matrix_homeserver_domain_desc', + alert: 'Federation_Matrix_homeserver_domain_alert', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_bridge_url', 'http://localhost:3300', { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_url', + i18nDescription: 'Federation_Matrix_bridge_url_desc', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_bridge_localpart', 'rocket.cat', { + readonly: true, + type: 'string', + i18nLabel: 'Federation_Matrix_bridge_localpart', + i18nDescription: 'Federation_Matrix_bridge_localpart_desc', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_registration_file', '', { + readonly: true, + type: 'code', + i18nLabel: 'Federation_Matrix_registration_file', + i18nDescription: 'Federation_Matrix_registration_file_desc', + alert: 'Federation_Matrix_registration_file_Alert', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_max_size_of_public_rooms_users', 100, { + readonly: true, + type: 'int', + i18nLabel: 'Federation_Matrix_max_size_of_public_rooms_users', + i18nDescription: 'Federation_Matrix_max_size_of_public_rooms_users_desc', + alert: 'Federation_Matrix_max_size_of_public_rooms_users_Alert', + modules: ['federation'], + public: true, + enterprise: true, + invalidValue: false, + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_configuration_status', 'Invalid', { + readonly: true, + hidden: true, + type: 'string', + i18nLabel: 'Federation_Matrix_configuration_status', + i18nDescription: 'Federation_Matrix_configuration_status_desc', + public: false, + enterprise: false, + invalidValue: '', + group: 'Federation', + section: 'Matrix Bridge', + }); + + await settingsRegistry.add('Federation_Matrix_check_configuration_button', 'checkFederationConfiguration', { + readonly: true, + hidden: true, + type: 'action', + actionText: 'Federation_Matrix_check_configuration', + public: false, + enterprise: false, + invalidValue: '', + group: 'Federation', + section: 'Matrix Bridge', + }); +}; diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Statistics.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Statistics.ts index b88b5f2755d6d..81a645329f8ee 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Statistics.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Statistics.ts @@ -1,5 +1,5 @@ import type { IMatrixFederationStatistics } from '@rocket.chat/core-typings'; -import { MatrixBridgedRoom, Rooms, Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import { settings } from '../../../../../../app/settings/server'; @@ -45,9 +45,7 @@ class RocketChatStatisticsAdapter { } async getAmountOfConnectedExternalServers(): Promise<{ quantity: number; servers: string[] }> { - const externalServers = await MatrixBridgedRoom.getExternalServerConnectedExcluding( - settings.get('Federation_Matrix_homeserver_domain'), - ); + const externalServers = await Rooms.countDistinctFederationRoomsExcluding(settings.get('Federation_Matrix_homeserver_domain')); return { quantity: externalServers.length, diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts index 0256b4f04fe85..dd9f0165b626a 100644 --- a/apps/meteor/server/services/federation/utils.ts +++ b/apps/meteor/server/services/federation/utils.ts @@ -1,35 +1,13 @@ import { settings } from '../../../app/settings/server'; export function isFederationEnabled(): boolean { - return settings.get('Federation_Matrix_enabled'); + return settings.get('Federation_Service_Enabled'); } -export function isFederationReady(): boolean { - return settings.get('Federation_Matrix_configuration_status') === 'Valid'; -} - -export function throwIfFederationNotEnabledOrNotReady(): void { +export function throwIfFederationNotEnabled(): void { if (!isFederationEnabled()) { throw new Error('Federation is not enabled'); } - - if (!isFederationReady()) { - throw new Error('Federation configuration is invalid'); - } -} - -export function throwIfFederationEnabledButNotReady(): void { - if (!isFederationEnabled()) { - return; - } - - throwIfFederationNotReady(); -} - -export function throwIfFederationNotReady(): void { - if (!isFederationReady()) { - throw new Error('Federation configuration is invalid'); - } } export class FederationMatrixInvalidConfigurationError extends Error { diff --git a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts index a8bbfb90fbb14..c3c00ce27dd81 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts @@ -1,15 +1,15 @@ -import { isMessageFromNativeFederation, isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings'; import type { AtLeast, IMessage, IRoom } from '@rocket.chat/core-typings'; import { isFederationEnabled } from '../../federation/utils'; export class FederationActions { - public static shouldPerformAction(message: IMessage, room: AtLeast): boolean { + public static shouldPerformAction(_message: IMessage, room: AtLeast): boolean { if (!isRoomFederated(room)) { return true; } - if (!isRoomNativeFederated(room) || !isMessageFromNativeFederation(message)) { + if (!isRoomNativeFederated(room)) { return false; } diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 828d2da19ead4..ec28bc8ef6fe8 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -85,6 +85,39 @@ export class MessageService extends ServiceClassInternal implements IMessageServ return executeSendMessage(fromId, { rid, msg }); } + async saveMessageFromFederation({ + fromId, + rid, + msg, + federation_event_id, + file, + files, + attachments, + thread, + }: { + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + file?: IMessage['file']; + files?: IMessage['files']; + attachments?: IMessage['attachments']; + thread?: { tmid: string; tshow: boolean }; + }): Promise { + return executeSendMessage(fromId, { + rid, + msg, + ...thread, + federation: { + eventId: federation_event_id, + version: 1, + }, + ...(file && { file }), + ...(files && { files }), + ...(attachments && { attachments }), + }); + } + async sendMessageWithValidation(user: IUser, message: Partial, room: Partial, upsert = false): Promise { return sendMessage(user, message, room, upsert); } diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index 3529fc071af3b..2dafdd99ace26 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -18,6 +18,7 @@ import { use } from '../../../app/settings/server/Middleware'; import { setValue, updateValue } from '../../../app/settings/server/raw'; import { getURL } from '../../../app/utils/server/getURL'; import { configureEmailInboxes } from '../../features/EmailInbox/EmailInbox'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { ListenersModule } from '../../modules/listeners/listeners.module'; type Callbacks = { @@ -292,4 +293,8 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { async getURL(path: string, params: Record = {}, cloudDeepLinkUrl?: string): Promise { return getURL(path, params, cloudDeepLinkUrl); } + + async getMessageURLToReplyTo(roomType: string, roomId: string, roomName: string, messageIdToReplyTo: string): Promise { + return getURL(`${roomCoordinator.getRouteLink(roomType, { rid: roomId, name: roomName })}?msg=${messageIdToReplyTo}`, { full: true }); + } } diff --git a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts index ccad0af4c6c45..082edcb71781e 100644 --- a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts @@ -1,17 +1,30 @@ -import { isRoomFederated, isRoomNativeFederated, type IRoom } from '@rocket.chat/core-typings'; +import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings'; +import type { IRoomNativeFederated, IRoom } from '@rocket.chat/core-typings'; -import { isFederationEnabled, throwIfFederationNotEnabledOrNotReady } from '../../federation/utils'; +import { throwIfFederationNotEnabled } from '../../federation/utils'; export class FederationActions { + public static shouldPerformFederationAction(room: IRoom): room is IRoomNativeFederated { + if (!isRoomFederated(room)) { + return false; + } + + if (!isRoomNativeFederated(room)) { + throw new Error('Room is federated but its not native implementation'); + } + + return true; + } + public static blockIfRoomFederatedButServiceNotReady(room: IRoom) { - if (!isRoomNativeFederated(room) && !isRoomFederated(room)) { + if (!isRoomFederated(room)) { return; } - if (!isFederationEnabled()) { - return; + if (!isRoomNativeFederated(room)) { + throw new Error('Room is federated but its not native implementation'); } - throwIfFederationNotEnabledOrNotReady(); + throwIfFederationNotEnabled(); } } diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index dfb118cf84758..e48eeedecf5c4 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -1,9 +1,10 @@ import { ServiceClassInternal, Authorization, MeteorError } from '@rocket.chat/core-services'; import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services'; import { type AtLeast, type IRoom, type IUser, isOmnichannelRoom, isRoomWithJoinCode } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { FederationActions } from './hooks/BeforeFederationActions'; +import { saveRoomName } from '../../../app/channel-settings/server'; import { saveRoomTopic } from '../../../app/channel-settings/server/functions/saveRoomTopic'; import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import @@ -11,7 +12,13 @@ import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUser import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { addRoomLeader } from '../../methods/addRoomLeader'; +import { addRoomModerator } from '../../methods/addRoomModerator'; +import { addRoomOwner } from '../../methods/addRoomOwner'; import { createDirectMessage } from '../../methods/createDirectMessage'; +import { removeRoomLeader } from '../../methods/removeRoomLeader'; +import { removeRoomModerator } from '../../methods/removeRoomModerator'; +import { removeRoomOwner } from '../../methods/removeRoomOwner'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; @@ -142,4 +149,60 @@ export class RoomService extends ServiceClassInternal implements IRoomService { async beforeTopicChange(room: IRoom): Promise { FederationActions.blockIfRoomFederatedButServiceNotReady(room); } + + async saveRoomName(roomId: string, userId: string, name: string) { + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('User not found'); + } + await saveRoomName(roomId, name, user); + } + + public async addUserRoleRoomScoped( + fromUserId: string, + userId: string, + roomId: string, + role: 'moderator' | 'owner' | 'leader' | 'user', + ): Promise { + if (role === 'moderator') { + await addRoomModerator(fromUserId, roomId, userId); + return; + } + + if (role === 'owner') { + await addRoomOwner(fromUserId, roomId, userId); + return; + } + + if (role === 'leader') { + await addRoomLeader(fromUserId, roomId, userId); + return; + } + + const sub = await Subscriptions.findByUserIdAndRoomIds(userId, [roomId], { projection: { roles: 1 } }).next(); + if (!sub) { + throw new Error('user and room subsciption not found'); + } + + if (!sub.roles) { + return; // 'user' role essentially + } + + for await (const currentRole of sub.roles) { + if (currentRole === 'owner') { + await removeRoomOwner(fromUserId, roomId, userId); + return; + } + + if (currentRole === 'leader') { + await removeRoomLeader(fromUserId, roomId, userId); + return; + } + + if (currentRole === 'moderator') { + await removeRoomModerator(fromUserId, roomId, userId); + return; + } + } + } } diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts new file mode 100644 index 0000000000000..2ae38944734ca --- /dev/null +++ b/apps/meteor/server/settings/federation-service.ts @@ -0,0 +1,88 @@ +import { generateEd25519RandomSecretKey } from '@rocket.chat/federation-matrix'; + +import { settingsRegistry } from '../../app/settings/server'; + +export const createFederationServiceSettings = async (): Promise => { + await settingsRegistry.addGroup('Federation', async function () { + await this.add('Federation_Service_Enabled', false, { + type: 'boolean', + public: true, + enterprise: true, + modules: ['federation'], + invalidValue: false, + alert: 'Federation_Service_Alert', + }); + + await this.add('Federation_Service_Domain', '', { + type: 'string', + public: false, + enterprise: true, + modules: ['federation'], + invalidValue: '', + alert: 'Federation_Service_Domain_Alert', + }); + + await this.add('Federation_Service_Matrix_Signing_Algorithm', 'ed25519', { + type: 'select', + public: false, + values: [{ key: 'ed25519', i18nLabel: 'ed25519' }], + enterprise: true, + modules: ['federation'], + invalidValue: 'ed25519', + }); + + await this.add('Federation_Service_Matrix_Signing_Version', '0', { + type: 'string', + public: false, + readonly: true, + enterprise: true, + modules: ['federation'], + invalidValue: '0', + }); + + const randomKey = generateEd25519RandomSecretKey().toString('base64'); + + // https://spec.matrix.org/v1.16/appendices/#signing-details + await this.add('Federation_Service_Matrix_Signing_Key', randomKey, { + type: 'password', + public: false, + enterprise: true, + modules: ['federation'], + invalidValue: '', + }); + + await this.add('Federation_Service_max_allowed_size_of_public_rooms_to_join', 100, { + type: 'int', + public: false, + alert: 'Federation_Service_max_allowed_size_of_public_rooms_to_join_Alert', + enterprise: true, + modules: ['federation'], + invalidValue: false, + }); + + await this.add('Federation_Service_Allow_List', '', { + type: 'string', + i18nLabel: 'Federation_Service_Allow_List', + i18nDescription: 'Federation_Service_Allow_List_Description', + public: false, + }); + + await this.add('Federation_Service_EDU_Process_Typing', true, { + type: 'boolean', + public: false, + enterprise: true, + modules: ['federation'], + invalidValue: false, + alert: 'Federation_Service_EDU_Process_Typing_Alert', + }); + + await this.add('Federation_Service_EDU_Process_Presence', false, { + type: 'boolean', + public: false, + enterprise: true, + modules: ['federation'], + invalidValue: false, + alert: 'Federation_Service_EDU_Process_Presence_Alert', + }); + }); +}; diff --git a/apps/meteor/server/settings/federation.ts b/apps/meteor/server/settings/federation.ts index 84c8553a936ed..aa440b78fb6ef 100644 --- a/apps/meteor/server/settings/federation.ts +++ b/apps/meteor/server/settings/federation.ts @@ -7,20 +7,24 @@ export const createFederationSettings = () => await this.section('Rocket.Chat Federation', async function () { await this.add('FEDERATION_Enabled', false, { type: 'boolean', + readonly: true, i18nLabel: 'Enabled', i18nDescription: 'FEDERATION_Enabled', - alert: 'This_is_a_deprecated_feature_alert', + alert: 'Old_Federation_Alert', public: true, }); await this.add('FEDERATION_Status', 'Disabled', { readonly: true, + hidden: true, type: 'string', i18nLabel: 'FEDERATION_Status', }); await this.add('FEDERATION_Domain', '', { type: 'string', + readonly: true, + hidden: true, i18nLabel: 'FEDERATION_Domain', i18nDescription: 'FEDERATION_Domain_Description', alert: 'FEDERATION_Domain_Alert', @@ -31,6 +35,7 @@ export const createFederationSettings = () => await this.add('FEDERATION_Public_Key', federationPublicKey || '', { readonly: true, + hidden: true, type: 'string', multiline: true, i18nLabel: 'FEDERATION_Public_Key', @@ -39,6 +44,8 @@ export const createFederationSettings = () => await this.add('FEDERATION_Discovery_Method', 'dns', { type: 'select', + readonly: true, + hidden: true, values: [ { key: 'dns', @@ -56,6 +63,7 @@ export const createFederationSettings = () => await this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', { type: 'action', + hidden: true, actionText: 'FEDERATION_Test_Setup', }); }); diff --git a/apps/meteor/server/settings/index.ts b/apps/meteor/server/settings/index.ts index 2a7973eddeece..91c8f403e2815 100644 --- a/apps/meteor/server/settings/index.ts +++ b/apps/meteor/server/settings/index.ts @@ -11,6 +11,7 @@ import { createDiscussionsSettings } from './discussions'; import { createE2ESettings } from './e2e'; import { createEmailSettings } from './email'; import { createFederationSettings } from './federation'; +import { createFederationServiceSettings } from './federation-service'; import { createFileUploadSettings } from './file-upload'; import { createGeneralSettings } from './general'; import { createIRCSettings } from './irc'; @@ -36,8 +37,10 @@ import { createUserDataSettings } from './userDataDownload'; import { createVConfSettings } from './video-conference'; import { createWebDavSettings } from './webdav'; import { createWebRTCSettings } from './webrtc'; +import { addMatrixBridgeFederationSettings } from '../services/federation/Settings'; await Promise.all([ + createFederationServiceSettings(), createAccountSettings(), createAnalyticsSettings(), createAssetsSettings(), @@ -50,7 +53,6 @@ await Promise.all([ createDiscussionsSettings(), createEmailSettings(), createE2ESettings(), - createFederationSettings(), createFileUploadSettings(), createGeneralSettings(), createIRCSettings(), @@ -77,3 +79,9 @@ await Promise.all([ createWebDavSettings(), createWebRTCSettings(), ]); + +// Run after all the other settings are created since it depends on some of them +await Promise.all([ + createFederationSettings(), // Deprecated and not used anymore. Kept for admin UI information purposes. Remove on 8.0 + addMatrixBridgeFederationSettings(), // Deprecated and not used anymore. Kept for admin UI information purposes. Remove on 8.0 +]); diff --git a/apps/meteor/startRocketChat.ts b/apps/meteor/startRocketChat.ts index 137a41e0c1b3e..3d9b803742bda 100644 --- a/apps/meteor/startRocketChat.ts +++ b/apps/meteor/startRocketChat.ts @@ -1,12 +1,19 @@ import { startLicense } from './ee/app/license/server/startup'; import { registerEEBroker } from './ee/server'; +import { startFederationService as startFederationMatrixService } from './ee/server/startup/federation'; const loadBeforeLicense = async () => { await registerEEBroker(); }; +const loadAfterLicense = async () => { + await startFederationMatrixService(); +}; + export const startRocketChat = async () => { await loadBeforeLicense(); await startLicense(); + + await loadAfterLicense(); }; diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index 65055925598d0..8b2bcbc783134 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -67,6 +67,9 @@ COPY ./packages/ui-kit/dist packages/ui-kit/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index f5c49abbaa217..ec7dd96967874 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -64,6 +64,9 @@ COPY ./packages/tracing/dist packages/tracing/dist COPY ./packages/ui-kit/package.json packages/ui-kit/package.json COPY ./packages/ui-kit/dist packages/ui-kit/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index e755df4aee719..a0758299c86f5 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -70,6 +70,9 @@ COPY ./packages/tsconfig packages/tsconfig COPY ./packages/ui-kit/package.json packages/ui-kit/package.json COPY ./packages/ui-kit/dist packages/ui-kit/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/federation-service/.eslintrc.json b/ee/apps/federation-service/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /dev/null +++ b/ee/apps/federation-service/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json new file mode 100644 index 0000000000000..744074f2e8fc3 --- /dev/null +++ b/ee/apps/federation-service/package.json @@ -0,0 +1,53 @@ +{ + "name": "@rocket.chat/federation-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat Federation service", + "main": "./dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} bun --watch run src/service.ts", + "start": "bun run src/service.ts", + "dev": "bun --watch run src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "dependencies": { + "@hono/node-server": "^1.14.4", + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:*", + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-matrix": "workspace:^", + "@rocket.chat/federation-sdk": "0.1.8", + "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/instance-status": "workspace:^", + "@rocket.chat/license": "workspace:^", + "@rocket.chat/models": "workspace:*", + "@rocket.chat/network-broker": "workspace:^", + "hono": "^3.11.0", + "pino": "^9.11.0", + "polka": "^0.5.2", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^4.17.17", + "eslint": "~8.45.0", + "pino-pretty": "^7.6.1", + "typescript": "^5.3.0" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat" +} diff --git a/ee/apps/federation-service/src/config.ts b/ee/apps/federation-service/src/config.ts new file mode 100644 index 0000000000000..b4afd9b8be330 --- /dev/null +++ b/ee/apps/federation-service/src/config.ts @@ -0,0 +1,3 @@ +export const config = { + port: parseInt(process.env.FEDERATION_SERVICE_PORT || '3030'), +}; diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts new file mode 100644 index 0000000000000..ce944b838ecc2 --- /dev/null +++ b/ee/apps/federation-service/src/service.ts @@ -0,0 +1,71 @@ +import 'reflect-metadata'; +import { serve } from '@hono/node-server'; +import { api, getConnection, getTrashCollection, Settings } from '@rocket.chat/core-services'; +import { InstanceStatus } from '@rocket.chat/instance-status'; +import { License } from '@rocket.chat/license'; +import { registerServiceModels } from '@rocket.chat/models'; +import { startBroker } from '@rocket.chat/network-broker'; +import { Hono } from 'hono'; + +import { config } from './config'; + +function handleHealthCheck(app: Hono) { + app.get('/health', async (c) => { + try { + const hasLicense = await License.hasModule('federation'); + const isEnabled = await Settings.get('Federation_Service_Enabled'); + + return c.json({ + status: 'ok', + license: hasLicense ? 'valid' : 'invalid', + settings: { + federation_enabled: isEnabled, + }, + }); + } catch (err) { + console.error('Service not healthy', err); + return c.json({ status: 'not healthy', error: (err as Error).message }, 500); + } + }); +} + +(async () => { + console.log('Starting federation-service on microservice mode'); + + const { db } = await getConnection(); + registerServiceModels(db, await getTrashCollection()); + + api.setBroker(startBroker()); + + await api.start(); + + const hasLicense = License.hasModule('federation'); + if (!hasLicense) { + throw new Error('Service requires a valid Enterprise license with the federation module'); + } + + const isEnabled = await Settings.get('Federation_Service_Enabled'); + if (!isEnabled) { + throw new Error('Service is disabled in settings (Federation_Service_Enabled = false)'); + } + + const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); + const federationMatrix = await FederationMatrix.create(InstanceStatus.id()); + api.registerService(federationMatrix); + + const app = new Hono(); + const { matrix, wellKnown } = federationMatrix.getAllRoutes(); + + app.mount('/_matrix', matrix.getHonoRouter().fetch); + app.mount('/.well-known', wellKnown.getHonoRouter().fetch); + + handleHealthCheck(app); + + serve({ + fetch: app.fetch, + port: config.port, + }); +})().catch((error) => { + console.error('Failed to start service:', error); + process.exit(1); +}); diff --git a/ee/apps/federation-service/tsconfig.json b/ee/apps/federation-service/tsconfig.json new file mode 100644 index 0000000000000..9ebd08665e2a4 --- /dev/null +++ b/ee/apps/federation-service/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "lib": ["es2020"], + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index c8a306a213480..5b566def3edc2 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -80,6 +80,9 @@ COPY ./packages/ui-kit/dist packages/ui-kit/dist COPY ./packages/i18n/package.json packages/i18n/package.json COPY ./packages/i18n/dist packages/i18n/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index a23a30b405a7f..aedd59b187c0e 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -68,6 +68,9 @@ COPY ./packages/tsconfig packages/tsconfig COPY ./packages/ui-kit/package.json packages/ui-kit/package.json COPY ./packages/ui-kit/dist packages/ui-kit/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index c8a306a213480..5b566def3edc2 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -80,6 +80,9 @@ COPY ./packages/ui-kit/dist packages/ui-kit/dist COPY ./packages/i18n/package.json packages/i18n/package.json COPY ./packages/i18n/dist packages/i18n/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index 0f017c5f90ef1..8fc4ab2acce2e 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -65,6 +65,9 @@ COPY ./packages/tsconfig packages/tsconfig COPY ./packages/ui-kit/package.json packages/ui-kit/package.json COPY ./packages/ui-kit/dist packages/ui-kit/dist +COPY ./packages/http-router/package.json packages/http-router/package.json +COPY ./packages/http-router/dist packages/http-router/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/packages/federation-matrix/.eslintrc.json b/ee/packages/federation-matrix/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /dev/null +++ b/ee/packages/federation-matrix/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/federation-matrix/.gitignore b/ee/packages/federation-matrix/.gitignore new file mode 100644 index 0000000000000..996e8eedb9a25 --- /dev/null +++ b/ee/packages/federation-matrix/.gitignore @@ -0,0 +1 @@ +.nyc_output diff --git a/ee/packages/federation-matrix/babel.config.js b/ee/packages/federation-matrix/babel.config.js new file mode 100644 index 0000000000000..7672dadf24ca2 --- /dev/null +++ b/ee/packages/federation-matrix/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], +}; diff --git a/ee/packages/federation-matrix/jest.config.ts b/ee/packages/federation-matrix/jest.config.ts new file mode 100644 index 0000000000000..5ee40fe48b7a3 --- /dev/null +++ b/ee/packages/federation-matrix/jest.config.ts @@ -0,0 +1,11 @@ +import server from '@rocket.chat/jest-presets/server'; +import type { Config } from 'jest'; + +export default { + preset: server.preset, + transformIgnorePatterns: [ + '/node_modules/@babel', + '/node_modules/@jest', + '/node_modules/(?!marked|@testing-library/)', + ], +} satisfies Config; diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json new file mode 100644 index 0000000000000..5d26e503b52c9 --- /dev/null +++ b/ee/packages/federation-matrix/package.json @@ -0,0 +1,57 @@ +{ + "name": "@rocket.chat/federation-matrix", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@babel/cli": "~7.26.0", + "@babel/core": "~7.26.0", + "@babel/preset-env": "~7.26.0", + "@babel/preset-typescript": "~7.26.0", + "@rocket.chat/eslint-config": "workspace:^", + "@types/emojione": "^2.2.9", + "@types/node": "~22.14.0", + "@types/sanitize-html": "^2", + "babel-jest": "~30.0.0", + "eslint": "~8.45.0", + "jest": "~30.0.0", + "pino-pretty": "^7.6.1", + "typescript": "~5.8.3" + }, + "scripts": { + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "test": "jest", + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "testunit": "jest", + "typecheck": "tsc --noEmit --skipLibCheck", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/FederationMatrix.js", + "typings": "./dist/FederationMatrix.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-sdk": "0.1.8", + "@rocket.chat/http-router": "workspace:^", + "@rocket.chat/license": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@vector-im/matrix-bot-sdk": "^0.7.1-element.6", + "emojione": "^4.5.0", + "marked": "^16.1.2", + "mongodb": "6.10.0", + "pino": "^9.11.0", + "reflect-metadata": "^0.2.2", + "sanitize-html": "^2.17.0", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3" + } +} diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts new file mode 100644 index 0000000000000..ea0f33eb5dfb6 --- /dev/null +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -0,0 +1,947 @@ +import 'reflect-metadata'; + +import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; +import { + isDeletedMessage, + isMessageFromMatrixFederation, + isQuoteAttachment, + isRoomNativeFederated, + isUserNativeFederated, + UserStatus, +} from '@rocket.chat/core-typings'; +import type { MessageQuoteAttachment, IMessage, IRoom, IUser, IRoomNativeFederated } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import { ConfigService, createFederationContainer, getAllServices } from '@rocket.chat/federation-sdk'; +import type { + EventID, + HomeserverEventSignatures, + HomeserverServices, + FederationContainerOptions, + FileMessageType, + PresenceState, +} from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { Logger } from '@rocket.chat/logger'; +import { Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; +import emojione from 'emojione'; + +import { getWellKnownRoutes } from './api/.well-known/server'; +import { getMatrixInviteRoutes } from './api/_matrix/invite'; +import { getKeyServerRoutes } from './api/_matrix/key/server'; +import { getMatrixMediaRoutes } from './api/_matrix/media'; +import { getMatrixProfilesRoutes } from './api/_matrix/profiles'; +import { getMatrixRoomsRoutes } from './api/_matrix/rooms'; +import { getMatrixSendJoinRoutes } from './api/_matrix/send-join'; +import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; +import { getFederationVersionsRoutes } from './api/_matrix/versions'; +import { isFederationDomainAllowedMiddleware } from './api/middlewares/isFederationDomainAllowed'; +import { isFederationEnabledMiddleware } from './api/middlewares/isFederationEnabled'; +import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; +import { registerEvents } from './events'; +import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; +import { MatrixMediaService } from './services/MatrixMediaService'; + +export const fileTypes: Record = { + image: 'm.image', + video: 'm.video', + audio: 'm.audio', + file: 'm.file', +}; + +export { generateEd25519RandomSecretKey } from '@rocket.chat/federation-sdk'; + +export class FederationMatrix extends ServiceClass implements IFederationMatrixService { + protected name = 'federation-matrix'; + + private eventHandler: Emitter; + + private homeserverServices: HomeserverServices; + + private serverName: string; + + private readonly logger = new Logger(this.name); + + private httpRoutes: { matrix: Router<'/_matrix'>; wellKnown: Router<'/.well-known'> }; + + private processEDUTyping = false; + + private processEDUPresence = false; + + private constructor(emitter?: Emitter) { + super(); + this.eventHandler = emitter || new Emitter(); + } + + static async create(instanceId: string, emitter?: Emitter): Promise { + const instance = new FederationMatrix(emitter); + const settingsSigningAlg = await Settings.get('Federation_Service_Matrix_Signing_Algorithm'); + const settingsSigningVersion = await Settings.get('Federation_Service_Matrix_Signing_Version'); + const settingsSigningKey = await Settings.get('Federation_Service_Matrix_Signing_Key'); + const serverHostname = (await Settings.get('Federation_Service_Domain')).trim(); + + instance.serverName = serverHostname; + + instance.processEDUTyping = await Settings.get('Federation_Service_EDU_Process_Typing'); + instance.processEDUPresence = await Settings.get('Federation_Service_EDU_Process_Presence'); + + const mongoUri = process.env.MONGO_URL || 'mongodb://localhost:3001/meteor'; + + const dbName = process.env.DATABASE_NAME || new URL(mongoUri).pathname.slice(1); + + const config = new ConfigService({ + instanceId, + serverName: serverHostname, + keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), + matrixDomain: serverHostname, + version: process.env.SERVER_VERSION || '1.0', + port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), + signingKey: `${settingsSigningAlg} ${settingsSigningVersion} ${settingsSigningKey}`, + signingKeyPath: process.env.CONFIG_FOLDER || './rocketchat.signing.key', + database: { + uri: mongoUri, + name: dbName, + poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10), + }, + media: { + maxFileSize: Number.parseInt(process.env.MEDIA_MAX_FILE_SIZE || '100', 10) * 1024 * 1024, + allowedMimeTypes: process.env.MEDIA_ALLOWED_MIME_TYPES?.split(',') || [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'text/plain', + 'application/pdf', + 'video/mp4', + 'audio/mpeg', + 'audio/ogg', + ], + enableThumbnails: process.env.MEDIA_ENABLE_THUMBNAILS === 'true' || true, + rateLimits: { + uploadPerMinute: Number.parseInt(process.env.MEDIA_UPLOAD_RATE_LIMIT || '10', 10), + downloadPerMinute: Number.parseInt(process.env.MEDIA_DOWNLOAD_RATE_LIMIT || '60', 10), + }, + }, + }); + + const containerOptions: FederationContainerOptions = { + emitter: instance.eventHandler, + }; + + await createFederationContainer(containerOptions, config); + instance.homeserverServices = getAllServices(); + MatrixMediaService.setHomeserverServices(instance.homeserverServices); + instance.buildMatrixHTTPRoutes(); + + instance.onEvent( + 'presence.status', + async ({ user }: { user: Pick }): Promise => { + if (!instance.processEDUPresence) { + return; + } + + if (!user.username || !user.status || user.username.includes(':')) { + return; + } + const localUser = await Users.findOneByUsername(user.username, { projection: { _id: 1, federated: 1, federation: 1 } }); + if (!localUser) { + return; + } + + if (!isUserNativeFederated(localUser)) { + return; + } + + // TODO: Check if it should exclude himself from the list + const roomsUserIsMemberOf = await Subscriptions.findUserFederatedRoomIds(localUser._id).toArray(); + const statusMap: Record = { + [UserStatus.ONLINE]: 'online', + [UserStatus.OFFLINE]: 'offline', + [UserStatus.AWAY]: 'unavailable', + [UserStatus.BUSY]: 'unavailable', + [UserStatus.DISABLED]: 'offline', + }; + void instance.homeserverServices.edu.sendPresenceUpdateToRooms( + [ + { + user_id: localUser.federation.mui, + presence: statusMap[user.status] || 'offline', + }, + ], + roomsUserIsMemberOf.map(({ externalRoomId }) => externalRoomId).filter(Boolean), + ); + }, + ); + + instance.logger.startup(`Federation Matrix Homeserver created for domain ${instance.serverName}`); + + return instance; + } + + private buildMatrixHTTPRoutes() { + const matrix = new Router('/_matrix'); + const wellKnown = new Router('/.well-known'); + + matrix + .use(isFederationEnabledMiddleware) + .use(isLicenseEnabledMiddleware) + .use(getKeyServerRoutes(this.homeserverServices)) + .use(getFederationVersionsRoutes(this.homeserverServices)) + .use(isFederationDomainAllowedMiddleware) + .use(getMatrixInviteRoutes(this.homeserverServices)) + .use(getMatrixProfilesRoutes(this.homeserverServices)) + .use(getMatrixRoomsRoutes(this.homeserverServices)) + .use(getMatrixSendJoinRoutes(this.homeserverServices)) + .use(getMatrixTransactionsRoutes(this.homeserverServices)) + .use(getMatrixMediaRoutes(this.homeserverServices)); + + wellKnown.use(isFederationEnabledMiddleware).use(isLicenseEnabledMiddleware).use(getWellKnownRoutes(this.homeserverServices)); + + this.httpRoutes = { matrix, wellKnown }; + } + + async created(): Promise { + try { + registerEvents(this.eventHandler, this.serverName, { typing: this.processEDUTyping, presence: this.processEDUPresence }); + } catch (error) { + this.logger.warn('Homeserver module not available, running in limited mode'); + } + } + + getAllRoutes() { + return this.httpRoutes; + } + + getServerName(): string { + return this.serverName; + } + + async createRoom(room: IRoom, owner: IUser, members: string[]): Promise<{ room_id: string; event_id: string }> { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room creation'); + throw new Error('Homeserver services not available'); + } + + if (room.t !== 'c' && room.t !== 'p') { + throw new Error('Room is not a public or private room'); + } + + try { + const matrixUserId = `@${owner.username}:${this.serverName}`; + const roomName = room.name || room.fname || 'Untitled Room'; + + // canonical alias computed from name + const matrixRoomResult = await this.homeserverServices.room.createRoom(matrixUserId, roomName, room.t === 'c' ? 'public' : 'invite'); + + this.logger.debug('Matrix room created:', matrixRoomResult); + + await Rooms.setAsFederated(room._id, { mrid: matrixRoomResult.room_id, origin: this.serverName }); + + for await (const member of members) { + if (member === owner.username) { + continue; + } + + // We are not generating bridged users for members outside of the current workspace + // They will be created when the invite is accepted + + await this.homeserverServices.invite.inviteUserToRoom(member, matrixRoomResult.room_id, matrixUserId); + } + + this.logger.debug('Room creation completed successfully', room._id); + + return matrixRoomResult; + } catch (error) { + this.logger.error('Failed to create room:', error); + throw error; + } + } + + async ensureFederatedUsersExistLocally(usernames: string[]): Promise { + try { + this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: usernames.length }); + + const federatedUsers = usernames.filter((username) => username?.includes(':') && username?.includes('@')); + for await (const username of federatedUsers) { + if (!username) { + continue; + } + + const existingUser = await Users.findOneByUsername(username); + if (existingUser) { + continue; + } + + await Users.create({ + 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(), + }); + } + } catch (error) { + this.logger.error('Failed to ensure federated users exist locally:', error); + } + } + + async createDirectMessageRoom(room: IRoom, members: (IUser | string)[], creatorId: IUser['_id']): Promise { + try { + this.logger.debug('Creating direct message room in Matrix', { roomId: room._id, memberCount: members.length }); + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping DM room creation'); + return; + } + + const creator = await Users.findOneById(creatorId); + if (!creator) { + throw new Error('Creator not found in members list'); + } + + const actualMatrixUserId = `@${creator.username}:${this.serverName}`; + + 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; + }); + 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}`; + } + const roomId = await this.homeserverServices.room.createDirectMessageRoom(actualMatrixUserId, otherMemberMatrixId); + matrixRoomResult = { room_id: roomId }; + } else { + // For group DMs (more than 2 members), create a private room + const roomName = room.name || room.fname || `Group chat with ${members.length} members`; + 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) { + 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 (members.length > 2) { + await this.homeserverServices.invite.inviteUserToRoom(memberMatrixUserId, matrixRoomResult.room_id, actualMatrixUserId); + } + } catch (error) { + this.logger.error('Error creating or updating bridged user for DM:', error); + } + } + await Rooms.setAsFederated(room._id, { + mrid: matrixRoomResult.room_id, + origin: this.serverName, + }); + this.logger.debug('Direct message room creation completed successfully', room._id); + } catch (error) { + this.logger.error('Failed to create direct message room:', error); + throw error; + } + } + + private getMatrixMessageType(mimeType?: string): FileMessageType { + const mainType = mimeType?.split('/')[0]; + if (!mainType) { + return fileTypes.file; + } + + return fileTypes[mainType] ?? fileTypes.file; + } + + private async handleFileMessage( + message: IMessage, + matrixRoomId: string, + matrixUserId: string, + matrixDomain: string, + ): Promise<{ eventId: string } | null> { + if (!message.files || message.files.length === 0) { + return null; + } + + try { + let lastEventId: { eventId: string } | null = null; + + // TODO handle multiple files, we currently save thumbs on files[], we need to flag them as thumb so we can ignore them here + const [file] = message.files; + + const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain, matrixRoomId); + + const msgtype = this.getMatrixMessageType(file.type); + const fileContent = { + body: file.name, + msgtype, + url: mxcUri, + info: { + mimetype: file.type, + size: file.size, + }, + }; + + lastEventId = await this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId); + + return lastEventId; + } catch (error) { + this.logger.error('Failed to handle file message', { + messageId: message._id, + error, + }); + throw error; + } + } + + private async handleTextMessage( + message: IMessage, + matrixRoomId: string, + matrixUserId: string, + matrixDomain: string, + ): Promise<{ eventId: string } | null> { + const parsedMessage = await toExternalMessageFormat({ + message: message.msg, + externalRoomId: matrixRoomId, + homeServerDomain: matrixDomain, + }); + + if (message.tmid) { + return this.handleThreadedMessage(message, matrixRoomId, matrixUserId, matrixDomain, parsedMessage); + } + + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + return this.handleQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain); + } + + return this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, matrixUserId); + } + + private async handleThreadedMessage( + message: IMessage, + matrixRoomId: string, + matrixUserId: string, + matrixDomain: string, + parsedMessage: string, + ): Promise<{ eventId: string } | null> { + if (!message.tmid) { + throw new Error('Thread message ID not found'); + } + + const threadRootMessage = await Messages.findOneById(message.tmid); + const threadRootEventId = threadRootMessage?.federation?.eventId; + + if (!threadRootEventId) { + this.logger.warn('Thread root event ID not found, sending as regular message'); + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + return this.handleQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain); + } + return this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, matrixUserId); + } + + const latestThreadMessage = await Messages.findLatestFederationThreadMessageByTmid(message.tmid, message._id); + const latestThreadEventId = latestThreadMessage?.federation?.eventId; + + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain); + if (!quoteMessage) { + throw new Error('Failed to retrieve quote message'); + } + return this.homeserverServices.message.sendReplyToInsideThreadMessage( + matrixRoomId, + quoteMessage.rawMessage, + quoteMessage.formattedMessage, + matrixUserId, + threadRootEventId, + quoteMessage.eventToReplyTo, + ); + } + + return this.homeserverServices.message.sendThreadMessage( + matrixRoomId, + message.msg, + parsedMessage, + matrixUserId, + threadRootEventId, + latestThreadEventId, + ); + } + + private async handleQuoteMessage( + message: IMessage, + matrixRoomId: string, + matrixUserId: string, + matrixDomain: string, + ): Promise<{ eventId: string } | null> { + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain); + if (!quoteMessage) { + throw new Error('Failed to retrieve quote message'); + } + return this.homeserverServices.message.sendReplyToMessage( + matrixRoomId, + quoteMessage.rawMessage, + quoteMessage.formattedMessage, + quoteMessage.eventToReplyTo, + matrixUserId, + ); + } + + async sendMessage(message: IMessage, room: IRoomNativeFederated, user: IUser): Promise { + try { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping message send'); + return; + } + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + let result; + if (message.files && message.files.length > 0) { + result = await this.handleFileMessage(message, room.federation.mrid, userMui, this.serverName); + } else { + result = await this.handleTextMessage(message, room.federation.mrid, userMui, this.serverName); + } + + if (!result) { + throw new Error('Failed to send message to Matrix - no result returned'); + } + + await Messages.setFederationEventIdById(message._id, result.eventId); + + this.logger.debug('Message sent to Matrix successfully:', result.eventId); + } catch (error) { + this.logger.error('Failed to send message to Matrix:', error); + throw error; + } + } + + private async getQuoteMessage( + message: IMessage, + matrixRoomId: string, + matrixUserId: string, + matrixDomain: string, + ): Promise<{ formattedMessage: string; rawMessage: string; eventToReplyTo: string } | undefined> { + if (!message.attachments) { + return; + } + const messageLink = ( + message.attachments.find((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link)) as MessageQuoteAttachment + ).message_link; + + if (!messageLink) { + return; + } + const messageToReplyToId = messageLink.includes('msg=') && messageLink?.split('msg=').pop(); + if (!messageToReplyToId) { + return; + } + const messageToReplyTo = await Messages.findOneById(messageToReplyToId); + if (!messageToReplyTo?.federation?.eventId) { + return; + } + + const { formattedMessage, message: rawMessage } = await toExternalQuoteMessageFormat({ + externalRoomId: matrixRoomId, + eventToReplyTo: messageToReplyTo.federation?.eventId, + originalEventSender: matrixUserId, + message: message.msg, + homeServerDomain: matrixDomain, + }); + + return { + formattedMessage, + rawMessage, + eventToReplyTo: messageToReplyTo.federation.eventId, + }; + } + + async deleteMessage(matrixRoomId: string, message: IMessage, uid: string): Promise { + try { + if (!isMessageFromMatrixFederation(message) || isDeletedMessage(message)) { + return; + } + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping message redaction'); + return; + } + + const matrixEventId = message.federation?.eventId; + if (!matrixEventId) { + throw new Error(`No Matrix event ID mapping found for message ${message._id}`); + } + + // TODO fix branded EventID and remove type casting + // TODO message.u?.username is not the user who removed the message + const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId as EventID, uid); + + this.logger.debug('Message Redaction sent to Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to send redaction to Matrix:', error); + throw error; + } + } + + async inviteUsersToRoom(room: IRoomNativeFederated, usersUserName: 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; + } + + await this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId); + }), + ); + } catch (error) { + this.logger.error({ msg: 'Failed to invite an user to Matrix:', err: error }); + throw error; + } + } + + async sendReaction(messageId: string, reaction: string, user: IUser): Promise { + try { + const message = await Messages.findOneById(messageId); + if (!message) { + throw new Error(`Message ${messageId} not found`); + } + + const room = await Rooms.findOneById(message.rid); + if (!room || !isRoomNativeFederated(room)) { + throw new Error(`No Matrix room mapping found for room ${message.rid}`); + } + + const matrixEventId = message.federation?.eventId; + if (!matrixEventId) { + throw new Error(`No Matrix event ID mapping found for message ${messageId}`); + } + + const reactionKey = emojione.shortnameToUnicode(reaction); + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + const eventId = await this.homeserverServices.message.sendReaction(room.federation.mrid, matrixEventId, reactionKey, userMui); + + await Messages.setFederationReactionEventId(user.username || '', messageId, reaction, eventId); + + this.logger.debug('Reaction sent to Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to send reaction to Matrix:', error); + throw error; + } + } + + async removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise { + try { + const message = await Messages.findOneById(messageId); + if (!message) { + this.logger.error(`Message ${messageId} not found`); + return; + } + + const targetEventId = message.federation?.eventId; + if (!targetEventId) { + this.logger.warn(`No federation event ID found for message ${messageId}`); + return; + } + + const room = await Rooms.findOneById(message.rid); + if (!room || !isRoomNativeFederated(room)) { + this.logger.error(`No Matrix room mapping found for room ${message.rid}`); + return; + } + + const reactionKey = emojione.shortnameToUnicode(reaction); + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + const reactionData = oldMessage.reactions?.[reaction]; + if (!reactionData?.federationReactionEventIds) { + return; + } + + for await (const [eventId, username] of Object.entries(reactionData.federationReactionEventIds)) { + if (username !== user.username) { + continue; + } + + const redactionEventId = await this.homeserverServices.message.unsetReaction( + room.federation.mrid, + eventId as EventID, + reactionKey, + userMui, + ); + if (!redactionEventId) { + this.logger.warn('No reaction event found to remove in Matrix'); + return; + } + + await Messages.unsetFederationReactionEventId(eventId, messageId, reaction); + break; + } + } catch (error) { + this.logger.error('Failed to remove reaction from Matrix:', error); + throw error; + } + } + + async getEventById(eventId: EventID): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available'); + return null; + } + + try { + return await this.homeserverServices.event.getEventById(eventId); + } catch (error) { + this.logger.error('Failed to get event by ID:', error); + throw error; + } + } + + async leaveRoom(roomId: string, user: IUser): Promise { + try { + const room = await Rooms.findOneById(roomId); + if (!room || !isRoomNativeFederated(room)) { + this.logger.debug(`Room ${roomId} is not federated, skipping leave operation`); + return; + } + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room leave'); + return; + } + + const actualMatrixUserId = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + await this.homeserverServices.room.leaveRoom(room.federation.mrid, actualMatrixUserId); + + this.logger.info(`User ${user.username} left Matrix room ${room.federation.mrid} successfully`); + } catch (error) { + this.logger.error('Failed to leave room in Matrix:', error); + throw error; + } + } + + async kickUser(room: IRoomNativeFederated, removedUser: IUser, userWhoRemoved: IUser): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping user kick'); + return; + } + + try { + const actualKickedMatrixUserId = isUserNativeFederated(removedUser) + ? removedUser.federation.mui + : `@${removedUser.username}:${this.serverName}`; + + const actualSenderMatrixUserId = isUserNativeFederated(userWhoRemoved) + ? userWhoRemoved.federation.mui + : `@${userWhoRemoved.username}:${this.serverName}`; + + await this.homeserverServices.room.kickUser( + room.federation.mrid, + actualKickedMatrixUserId, + actualSenderMatrixUserId, + `Kicked by ${userWhoRemoved.username}`, + ); + + this.logger.info(`User ${removedUser.username} was kicked from Matrix room ${room.federation.mrid} by ${userWhoRemoved.username}`); + } catch (error) { + this.logger.error('Failed to kick user from Matrix room:', error); + throw error; + } + } + + async updateMessage(room: IRoomNativeFederated, message: IMessage): Promise { + try { + const matrixEventId = message.federation?.eventId; + if (!matrixEventId) { + throw new Error(`No Matrix event ID mapping found for message ${message._id}`); + } + + const user = await Users.findOneById(message.u._id, { projection: { _id: 1, username: 1, federation: 1, federated: 1 } }); + if (!user) { + this.logger.error(`No user found for ID ${message.u._id}`); + return; + } + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + const parsedMessage = await toExternalMessageFormat({ + message: message.msg, + externalRoomId: room.federation.mrid, + homeServerDomain: this.serverName, + }); + const eventId = await this.homeserverServices.message.updateMessage( + room.federation.mrid, + message.msg, + parsedMessage, + userMui, + matrixEventId, + ); + + this.logger.debug('Message updated in Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to update message in Matrix:', error); + throw error; + } + } + + async updateRoomName(rid: string, displayName: string, user: IUser): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room name update'); + return; + } + + const room = await Rooms.findOneById(rid); + if (!room || !isRoomNativeFederated(room)) { + throw new Error(`No Matrix room mapping found for room ${rid}`); + } + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + await this.homeserverServices.room.updateRoomName(room.federation.mrid, displayName, userMui); + } + + async updateRoomTopic(room: IRoomNativeFederated, topic: string, user: IUser): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room topic update'); + + return; + } + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + await this.homeserverServices.room.setRoomTopic(room.federation.mrid, userMui, topic); + } + + async addUserRoleRoomScoped( + room: IRoomNativeFederated, + senderId: string, + userId: string, + role: 'moderator' | 'owner' | 'leader' | 'user', + ): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping user role room scoped'); + return; + } + + if (role === 'leader') { + throw new Error('Leader role is not supported'); + } + + const user = await Users.findOneById(userId); + if (!user) { + throw new Error(`No user found for ID ${userId}`); + } + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; + + const userSender = await Users.findOneById(senderId); + if (!userSender) { + throw new Error(`No user found for ID ${senderId}`); + } + const senderMui = isUserNativeFederated(userSender) ? userSender.federation.mui : `@${userSender.username}:${this.serverName}`; + + let powerLevel = 0; + if (role === 'owner') { + powerLevel = 100; + } else if (role === 'moderator') { + powerLevel = 50; + } + await this.homeserverServices.room.setPowerLevelForUser(room.federation.mrid, senderMui, userMui, powerLevel); + } + + async notifyUserTyping(rid: string, user: string, isTyping: boolean) { + if (!this.processEDUTyping) { + return; + } + + if (!rid || !user) { + return; + } + const room = await Rooms.findOneById(rid); + if (!room || !isRoomNativeFederated(room)) { + return; + } + const localUser = await Users.findOneByUsername>(user, { + projection: { _id: 1, username: 1, federation: 1, federated: 1 }, + }); + + if (!localUser) { + return; + } + + const userMui = isUserNativeFederated(localUser) ? localUser.federation.mui : `@${localUser.username}:${this.serverName}`; + + void this.homeserverServices.edu.sendTypingNotification(room.federation.mrid, userMui, isTyping); + } +} diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts new file mode 100644 index 0000000000000..c4a8920ff96f3 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -0,0 +1,46 @@ +import { Router } from "@rocket.chat/http-router"; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; +import { createHash } from 'node:crypto'; + +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; + +const WellKnownServerResponseSchema = { + type: 'object', + properties: { + 'm.server': { + type: 'string', + description: 'Matrix server address with port' + } + }, + required: ['m.server'] +}; + +const isWellKnownServerResponseProps = ajv.compile(WellKnownServerResponseSchema); + +// TODO: After changing the domain setting this route is still reporting the old domain until the server is restarted +// TODO: this is wrong, is siteurl !== domain this path should return 404. this path is to discover the final address, domain being the "proxy" and siteurl the final destination, if domain is different, well-known should be served there, not here. +export const getWellKnownRoutes = (services: HomeserverServices) => { + const { wellKnown } = services; + + return new Router('/matrix').get('/server', { + response: { + 200: isWellKnownServerResponseProps + }, + tags: ['Well-Known'], + license: ['federation'] + }, async (c) => { + const responseData = wellKnown.getWellKnownHostData(); + + const etag = createHash('md5') + .update(JSON.stringify(responseData)) + .digest('hex'); + + c.header('ETag', etag); + c.header('Content-Type', 'application/json'); + + return { + body: responseData, + statusCode: 200, + }; + }); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts new file mode 100644 index 0000000000000..f61ace906b70b --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -0,0 +1,351 @@ +import { Room } from '@rocket.chat/core-services'; +import type { IUser, UserStatus } from '@rocket.chat/core-typings'; +import type { + HomeserverServices, + RoomService, + StateService, + PduMembershipEventContent, + PersistentEventBase, + RoomVersion, +} from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { Rooms, Users } from '@rocket.chat/models'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const EventBaseSchema = { + type: 'object', + properties: { + type: { + type: 'string', + description: 'Event type', + }, + content: { + type: 'object', + description: 'Event content', + }, + sender: { + type: 'string', + }, + room_id: { + type: 'string', + }, + origin_server_ts: { + type: 'number', + }, + depth: { + type: 'number', + }, + prev_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Previous events in the room', + }, + auth_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Authorization events', + }, + origin: { + type: 'string', + description: 'Origin server', + }, + hashes: { + type: 'object', + nullable: true, + }, + signatures: { + type: 'object', + nullable: true, + }, + unsigned: { + type: 'object', + description: 'Unsigned data', + nullable: true, + }, + }, + required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events', 'origin'], +}; + +const MembershipEventContentSchema = { + type: 'object', + properties: { + membership: { + type: 'string', + }, + displayname: { + type: 'string', + nullable: true, + }, + avatar_url: { + type: 'string', + nullable: true, + }, + }, + required: ['membership'], +}; + +const RoomMemberEventSchema = { + type: 'object', + allOf: [ + EventBaseSchema, + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'm.room.member', + }, + content: MembershipEventContentSchema, + state_key: { + type: 'string', + }, + }, + required: ['type', 'content', 'state_key'], + }, + ], +}; + +const ProcessInviteParamsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + eventId: { + type: 'string', + }, + }, + required: ['roomId', 'eventId'], +}; + +const isProcessInviteParamsProps = ajv.compile(ProcessInviteParamsSchema); + +const ProcessInviteResponseSchema = { + type: 'object', + properties: { + event: RoomMemberEventSchema, + }, + required: ['event'], +}; + +const isProcessInviteResponseProps = ajv.compile(ProcessInviteResponseSchema); + +// 5 seconds +// 25 seconds +// 625 seconds = 10 minutes 25 seconds // max +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); + setTimeout(() => { + runWithBackoff(fn, delay * 1000); + }, delay); + } +} + +async function joinRoom({ + inviteEvent, + user, // ours trying to join the room + room, + state, +}: { + inviteEvent: PersistentEventBase; + user: IUser; + room: RoomService; + state: StateService; +}) { + // from the response we get the event + if (!inviteEvent.stateKey) { + throw new Error('join event has missing state key, unable to determine user to join'); + } + + // backoff needed for this call, can fail + await room.joinUser(inviteEvent.roomId, inviteEvent.stateKey); + + // now we create the room we saved post joining + const matrixRoom = await state.getFullRoomState2(inviteEvent.roomId); + if (!matrixRoom) { + throw new Error('room not found not processing invite'); + } + + // we only understand these two types of rooms, plus direct messages + const isDM = inviteEvent.getContent().is_direct; + + if (!isDM && !matrixRoom.isPublic() && !matrixRoom.isInviteOnly()) { + throw new Error('room is neither public, private, nor direct message - rocketchat is unable to join for now'); + } + + // 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; + + // create locally + if (!senderUser) { + const createdUser = await Users.insertOne({ + // let the _id auto generate we deal with usernames + 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; + } + + if (!senderUserId) { + throw new Error('Sender user ID not found'); + } + + let internalRoomId: string; + + const internalMappedRoom = await Rooms.findOne({ 'federation.mrid': inviteEvent.roomId }); + + if (!internalMappedRoom) { + let roomType: 'c' | 'p' | 'd'; + + if (isDM) { + roomType = 'd'; + } else if (matrixRoom.isPublic()) { + roomType = 'c'; + } else if (matrixRoom.isInviteOnly()) { + roomType = 'p'; + } else { + throw new Error('room is neither public, private, nor direct message - rocketchat is unable to join for now'); + } + + let ourRoom: { _id: string }; + + if (isDM) { + const senderUser = await Users.findOneById(senderUserId, { projection: { _id: 1, username: 1 } }); + const inviteeUser = user; + + if (!senderUser?.username) { + throw new Error('Sender user not found'); + } + if (!inviteeUser?.username) { + throw new Error('Invitee user not found'); + } + + ourRoom = await Room.create(senderUserId, { + type: roomType, + name: inviteEvent.sender, + members: [senderUser.username, inviteeUser.username], + options: { + federatedRoomId: inviteEvent.roomId, + creator: senderUserId, + }, + extraData: { + federated: true, + }, + }); + } else { + const roomFname = `${matrixRoom.name}:${matrixRoom.origin}`; + const roomName = inviteEvent.roomId.replace('!', '').replace(':', '_'); + + ourRoom = await Room.create(senderUserId, { + type: roomType, + name: roomName, + options: { + federatedRoomId: inviteEvent.roomId, + creator: senderUserId, + }, + extraData: { + federated: true, + fname: roomFname, + }, + }); + } + + internalRoomId = ourRoom._id; + } else { + internalRoomId = internalMappedRoom._id; + } + + 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)); +} + +export const getMatrixInviteRoutes = (services: HomeserverServices) => { + const { invite, state, room } = services; + + return new Router('/federation').put( + '/v2/invite/:roomId/:eventId', + { + body: ajv.compile({ type: 'object' }), // TODO: add schema from room package. + params: isProcessInviteParamsProps, + response: { + 200: isProcessInviteResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId, eventId } = c.req.param(); + const { event, room_version: roomVersion } = await c.req.json(); + + const userToCheck = event.state_key as string; + + if (!userToCheck) { + throw new Error('join event has missing state key, unable to determine user to join'); + } + + const [username /* domain */] = userToCheck.split(':'); + + // TODO: check domain + + const ourUser = await Users.findOneByUsername(username.slice(1)); + + if (!ourUser) { + throw new Error('user not found not processing invite'); + } + + const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); + + setTimeout( + () => { + void startJoiningRoom({ + inviteEvent, + user: ourUser, + room, + state, + }); + }, + inviteEvent.event.content.is_direct ? 2000 : 0, + ); + + return { + body: { + event: inviteEvent.event, + }, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/key/server.ts b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts new file mode 100644 index 0000000000000..a2b6ba144151f --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts @@ -0,0 +1,56 @@ +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const ServerKeyResponseSchema = { + type: 'object', + properties: { + old_verify_keys: { + type: 'object', + description: 'Old verification keys', + }, + server_name: { + type: 'string', + description: 'Matrix server name', + }, + signatures: { + type: 'object', + description: 'Server signatures', + }, + valid_until_ts: { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', + }, + verify_keys: { + type: 'object', + description: 'Current verification keys', + }, + }, + required: ['old_verify_keys', 'server_name', 'signatures', 'valid_until_ts', 'verify_keys'], +}; + +const isServerKeyResponseProps = ajv.compile(ServerKeyResponseSchema); + +export const getKeyServerRoutes = (services: HomeserverServices) => { + const { server } = services; + + return new Router('/key').get( + '/v2/server', + { + response: { + 200: isServerKeyResponseProps, + }, + tags: ['Key'], + license: ['federation'], + }, + async () => { + const response = await server.getSignedServerKey(); + + return { + body: response, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/media.ts b/ee/packages/federation-matrix/src/api/_matrix/media.ts new file mode 100644 index 0000000000000..ac22bf76defa4 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/media.ts @@ -0,0 +1,153 @@ +import crypto from 'crypto'; + +import type { IUpload } from '@rocket.chat/core-typings'; +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { MatrixMediaService } from '../../services/MatrixMediaService'; +import { canAccessMedia } from '../middlewares'; + +const MediaDownloadParamsSchema = { + type: 'object', + properties: { + mediaId: { type: 'string' }, + }, + required: ['mediaId'], + additionalProperties: false, +}; + +const ErrorResponseSchema = { + type: 'object', + properties: { + errcode: { type: 'string' }, + error: { type: 'string' }, + }, + required: ['errcode', 'error'], +}; + +const BufferResponseSchema = { + type: 'object', + description: 'Raw file buffer or multipart response', +}; + +const isMediaDownloadParamsProps = ajv.compile(MediaDownloadParamsSchema); +const isErrorResponseProps = ajv.compile(ErrorResponseSchema); +const isBufferResponseProps = ajv.compile(BufferResponseSchema); + +const SECURITY_HEADERS = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Content-Security-Policy': "default-src 'none'; img-src 'self'; media-src 'self'", + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', +}; + +function createMultipartResponse( + buffer: Buffer, + mimeType: string, + fileName: string, + metadata: Record = {}, +): { body: Buffer; contentType: string } { + const boundary = crypto.randomBytes(16).toString('hex'); + const parts: string[] = []; + + parts.push(`--${boundary}`, 'Content-Type: application/json', '', JSON.stringify(metadata)); + parts.push(`--${boundary}`, `Content-Type: ${mimeType}`, `Content-Disposition: attachment; filename="${fileName}"`, ''); + + const headerBuffer = Buffer.from(`${parts.join('\r\n')}\r\n`); + const endBoundary = Buffer.from(`\r\n--${boundary}--\r\n`); + + return { + body: Buffer.concat([headerBuffer, buffer, endBoundary]), + contentType: `multipart/mixed; boundary=${boundary}`, + }; +} + +async function getMediaFile(mediaId: string, serverName: string): Promise<{ file: IUpload; buffer: Buffer } | null> { + const file = await MatrixMediaService.getLocalFileForMatrixNode(mediaId, serverName); + if (!file) { + return null; + } + + const buffer = await MatrixMediaService.getLocalFileBuffer(file); + return { file, buffer }; +} + +export const getMatrixMediaRoutes = (homeserverServices: HomeserverServices) => { + const { config, federationAuth } = homeserverServices; + const router = new Router('/federation'); + + router.get( + '/v1/media/download/:mediaId', + { + params: isMediaDownloadParamsProps, + response: { + 200: isBufferResponseProps, + 401: isErrorResponseProps, + 403: isErrorResponseProps, + 404: isErrorResponseProps, + 429: isErrorResponseProps, + 500: isErrorResponseProps, + }, + tags: ['Federation', 'Media'], + }, + canAccessMedia(federationAuth), + async (c) => { + try { + const { mediaId } = c.req.param(); + const { serverName } = config; + + // TODO: Add file streaming support + const result = await getMediaFile(mediaId, serverName); + if (!result) { + return { + statusCode: 404, + body: { errcode: 'M_NOT_FOUND', error: 'Media not found' }, + }; + } + + const { file, buffer } = result; + + const mimeType = file.type || 'application/octet-stream'; + const fileName = file.name || mediaId; + + const multipartResponse = createMultipartResponse(buffer, mimeType, fileName); + + return { + statusCode: 200, + headers: { + ...SECURITY_HEADERS, + 'content-type': multipartResponse.contentType, + 'content-length': String(multipartResponse.body.length), + }, + body: multipartResponse.body, + }; + } catch (error) { + return { + statusCode: 500, + body: { errcode: 'M_UNKNOWN', error: 'Internal server error' }, + }; + } + }, + ); + + router.get( + '/v1/media/thumbnail/:mediaId', + { + params: isMediaDownloadParamsProps, + response: { + 404: isErrorResponseProps, + }, + tags: ['Federation', 'Media'], + }, + async () => ({ + statusCode: 404, + body: { + errcode: 'M_UNRECOGNIZED', + error: 'This endpoint is not implemented on the homeserver side', + }, + }), + ); + + return router; +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts new file mode 100644 index 0000000000000..57bd41fc482af --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -0,0 +1,491 @@ +import type { HomeserverServices, RoomVersion } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const UsernameSchema = { + type: 'string', + pattern: '^@[A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix user ID in format @user:server.com', +}; + +const RoomIdSchema = { + type: 'string', + pattern: '^![A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix room ID in format !room:server.com', +}; + +const TimestampSchema = { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', +}; + +const ServerNameSchema = { + type: 'string', + description: 'Matrix server name', +}; + +const QueryProfileQuerySchema = { + type: 'object', + properties: { + user_id: UsernameSchema, + field: { + type: 'string', + enum: ['displayname', 'avatar_url'], + description: 'Profile field to query', + nullable: true, + }, + }, + required: ['user_id'], + additionalProperties: false, +}; + +const isQueryProfileQueryProps = ajv.compile(QueryProfileQuerySchema); + +const QueryProfileResponseSchema = { + type: 'object', + properties: { + displayname: { + type: 'string', + description: 'User display name', + nullable: true, + }, + avatar_url: { + type: 'string', + description: 'User avatar URL', + nullable: true, + }, + }, +}; + +const isQueryProfileResponseProps = ajv.compile(QueryProfileResponseSchema); + +const QueryKeysBodySchema = { + type: 'object', + properties: { + device_keys: { + type: 'object', + description: 'Device keys to query', + }, + }, + required: ['device_keys'], +}; + +const isQueryKeysBodyProps = ajv.compile(QueryKeysBodySchema); + +const QueryKeysResponseSchema = { + type: 'object', + properties: { + device_keys: { + type: 'object', + description: 'Device keys for the requested users', + }, + }, + required: ['device_keys'], +}; + +const isQueryKeysResponseProps = ajv.compile(QueryKeysResponseSchema); + +const GetDevicesParamsSchema = { + type: 'object', + properties: { + userId: UsernameSchema, + }, + required: ['userId'], + additionalProperties: false, +}; + +const isGetDevicesParamsProps = ajv.compile(GetDevicesParamsSchema); + +const GetDevicesResponseSchema = { + type: 'object', + properties: { + user_id: UsernameSchema, + stream_id: { + type: 'number', + description: 'Device list stream ID', + }, + devices: { + type: 'array', + items: { + type: 'object', + properties: { + device_id: { + type: 'string', + description: 'Device ID', + }, + display_name: { + type: 'string', + description: 'Device display name', + nullable: true, + }, + last_seen_ip: { + type: 'string', + description: 'Last seen IP address', + nullable: true, + }, + last_seen_ts: { + ...TimestampSchema, + nullable: true, + }, + }, + required: ['device_id'], + }, + description: 'List of devices for the user', + }, + }, + required: ['user_id', 'stream_id', 'devices'], +}; + +const isGetDevicesResponseProps = ajv.compile(GetDevicesResponseSchema); + +const MakeJoinParamsSchema = { + type: 'object', + properties: { + roomId: RoomIdSchema, + userId: UsernameSchema, + }, + required: ['roomId', 'userId'], +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isMakeJoinParamsProps = ajv.compile(MakeJoinParamsSchema); + +const MakeJoinQuerySchema = { + type: 'object', + properties: { + ver: { + type: 'array', + items: { + type: 'string', + }, + minItems: 0, + description: 'Supported room versions', + }, + }, +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isMakeJoinQueryProps = ajv.compile(MakeJoinQuerySchema); + +const MakeJoinResponseSchema = { + type: 'object', + properties: { + room_version: { + type: 'string', + description: 'Room version', + }, + event: { + type: 'object', + properties: { + content: { + type: 'object', + properties: { + membership: { + type: 'string', + const: 'join', + }, + join_authorised_via_users_server: { + type: 'string', + nullable: true, + }, + }, + required: ['membership'], + }, + room_id: RoomIdSchema, + sender: UsernameSchema, + state_key: UsernameSchema, + type: { + type: 'string', + const: 'm.room.member', + }, + origin_server_ts: TimestampSchema, + origin: ServerNameSchema, + depth: { + type: 'number', + description: 'Depth of the event in the DAG', + nullable: true, + }, + prev_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Previous events in the room', + nullable: true, + }, + auth_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Authorization events', + nullable: true, + }, + hashes: { + type: 'object', + properties: { + sha256: { + type: 'string', + description: 'SHA256 hash of the event', + }, + }, + required: ['sha256'], + nullable: true, + }, + signatures: { + type: 'object', + description: 'Event signatures by server and key ID', + nullable: true, + }, + unsigned: { + type: 'object', + description: 'Unsigned data', + nullable: true, + }, + }, + required: ['content', 'room_id', 'sender', 'state_key', 'type', 'origin_server_ts', 'origin'], + }, + }, + required: ['room_version', 'event'], +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isMakeJoinResponseProps = ajv.compile(MakeJoinResponseSchema); + +const GetMissingEventsParamsSchema = { + type: 'object', + properties: { + roomId: RoomIdSchema, + }, + required: ['roomId'], +}; + +const isGetMissingEventsParamsProps = ajv.compile(GetMissingEventsParamsSchema); + +const GetMissingEventsBodySchema = { + type: 'object', + properties: { + earliest_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Earliest events', + }, + latest_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Latest events', + }, + limit: { + type: 'number', + minimum: 1, + maximum: 100, + description: 'Maximum number of events to return', + }, + }, + required: ['earliest_events', 'latest_events', 'limit'], +}; + +const isGetMissingEventsBodyProps = ajv.compile(GetMissingEventsBodySchema); + +const GetMissingEventsResponseSchema = { + type: 'object', + properties: { + events: { + type: 'array', + items: { + type: 'object', + }, + description: 'Missing events', + }, + }, + required: ['events'], +}; + +const isGetMissingEventsResponseProps = ajv.compile(GetMissingEventsResponseSchema); + +const EventAuthParamsSchema = { + type: 'object', + properties: { + roomId: RoomIdSchema, + eventId: { + type: 'string', + description: 'Event ID', + }, + }, + required: ['roomId', 'eventId'], +}; + +const isEventAuthParamsProps = ajv.compile(EventAuthParamsSchema); + +const EventAuthResponseSchema = { + type: 'object', + properties: { + auth_chain: { + type: 'array', + items: { + type: 'object', + }, + description: 'Authorization chain for the event', + }, + }, + required: ['auth_chain'], +}; + +const isEventAuthResponseProps = ajv.compile(EventAuthResponseSchema); + +export const getMatrixProfilesRoutes = (services: HomeserverServices) => { + const { profile } = services; + + return new Router('/federation') + .get( + '/v1/query/profile', + { + query: isQueryProfileQueryProps, + response: { + 200: isQueryProfileResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { user_id: userId, field } = c.req.query(); + + const response = await profile.queryProfile(userId); + + if (field) { + return { + body: { + [field]: response[field as 'displayname' | 'avatar_url'] || null, + }, + statusCode: 200, + }; + } + + return { + body: response, + statusCode: 200, + }; + }, + ) + .post( + '/v1/user/keys/query', + { + body: isQueryKeysBodyProps, + response: { + 200: isQueryKeysResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const body = await c.req.json(); + + const response = await profile.queryKeys(body.device_keys); + + return { + body: response, + statusCode: 200, + }; + }, + ) + .get( + '/v1/user/devices/:userId', + { + params: isGetDevicesParamsProps, + response: { + 200: isGetDevicesResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { userId } = c.req.param(); + + const response = await profile.getDevices(userId); + + return { + body: response, + statusCode: 200, + }; + }, + ) + .get( + '/v1/make_join/:roomId/:userId', + { + params: isMakeJoinParamsProps, + query: isMakeJoinQueryProps, + response: { + 200: isMakeJoinResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId, userId } = c.req.param(); + const url = new URL(c.req.url); + const verParams = url.searchParams.getAll('ver'); + + const response = await profile.makeJoin(roomId, userId, verParams.length > 0 ? (verParams as RoomVersion[]) : ['1']); + + return { + body: { + room_version: response.room_version, + event: response.event, + }, + statusCode: 200, + }; + }, + ) + .post( + '/v1/get_missing_events/:roomId', + { + params: isGetMissingEventsParamsProps, + body: isGetMissingEventsBodyProps, + response: { + 200: isGetMissingEventsResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId } = c.req.param(); + const body = await c.req.json(); + + const response = await profile.getMissingEvents(roomId, body.earliest_events, body.latest_events, body.limit); + + return { + body: response, + statusCode: 200, + }; + }, + ) + .get( + '/v1/event_auth/:roomId/:eventId', + { + params: isEventAuthParamsProps, + response: { + 200: isEventAuthResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId, eventId } = c.req.param(); + + const response = await profile.eventAuth(roomId, eventId); + + return { + body: response, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts new file mode 100644 index 0000000000000..ab6d05f29a58b --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts @@ -0,0 +1,207 @@ +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const PublicRoomsQuerySchema = { + type: 'object', + properties: { + include_all_networks: { + type: 'boolean', + description: 'Include all networks (ignored)', + }, + limit: { + type: 'number', + description: 'Maximum number of rooms to return', + }, + }, + required: ['include_all_networks', 'limit'], +}; + +const isPublicRoomsQueryProps = ajv.compile(PublicRoomsQuerySchema); + +const RoomObjectSchema = { + type: 'object', + properties: { + avatar_url: { + type: 'string', + description: 'Room avatar URL', + }, + canonical_alias: { + type: 'string', + description: 'Room canonical alias', + nullable: true, + }, + guest_can_join: { + type: 'boolean', + description: 'Whether guests can join the room', + }, + join_rule: { + type: 'string', + description: 'Room join rule', + }, + name: { + type: 'string', + description: 'Room name', + }, + num_joined_members: { + type: 'number', + description: 'Number of joined members', + nullable: true, + }, + room_id: { + type: 'string', + description: 'Room ID', + }, + room_type: { + type: 'string', + description: 'Room type', + nullable: true, + }, + topic: { + type: 'string', + description: 'Room topic', + nullable: true, + }, + world_readable: { + type: 'boolean', + description: 'Whether the room is world readable', + }, + }, + required: ['avatar_url', 'guest_can_join', 'join_rule', 'name', 'room_id', 'world_readable'], +}; + +const PublicRoomsResponseSchema = { + type: 'object', + properties: { + chunk: { + type: 'array', + items: RoomObjectSchema, + description: 'Array of public rooms', + }, + }, + required: ['chunk'], +}; + +const isPublicRoomsResponseProps = ajv.compile(PublicRoomsResponseSchema); + +const PublicRoomsPostBodySchema = { + type: 'object', + properties: { + include_all_networks: { + type: 'string', + description: 'Include all networks (ignored)', + nullable: true, + }, + limit: { + type: 'number', + description: 'Maximum number of rooms to return', + nullable: true, + }, + filter: { + type: 'object', + properties: { + generic_search_term: { + type: 'string', + description: 'Generic search term for filtering rooms', + nullable: true, + }, + room_types: { + type: 'array', + items: { + type: ['string', 'null'], + }, + description: 'Array of room types to filter by', + nullable: true, + }, + }, + }, + }, + required: ['filter'], +}; + +const isPublicRoomsPostBodyProps = ajv.compile(PublicRoomsPostBodySchema); + +export const getMatrixRoomsRoutes = (services: HomeserverServices) => { + const { state } = services; + + return new Router('/federation') + .get( + '/v1/publicRooms', + { + query: isPublicRoomsQueryProps, + response: { + 200: isPublicRoomsResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async () => { + const defaultObj = { + join_rule: 'public', + guest_can_join: false, // trying to reduce required endpoint hits + world_readable: false, // ^^^ + avatar_url: '', // ?? don't have any yet + }; + + const publicRooms = await state.getAllPublicRoomIdsAndNames(); + + return { + body: { + chunk: publicRooms.map((room) => ({ + ...defaultObj, + ...room, + })), + }, + statusCode: 200, + }; + }, + ) + .post( + '/v1/publicRooms', + { + body: isPublicRoomsPostBodyProps, + response: { + 200: isPublicRoomsResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const body = await c.req.json(); + + const defaultObj = { + join_rule: 'public', + guest_can_join: false, // trying to reduce required endpoint hits + world_readable: false, // ^^^ + avatar_url: '', // ?? don't have any yet + }; + + const { filter } = body; + + const publicRooms = await state.getAllPublicRoomIdsAndNames(); + + return { + body: { + chunk: publicRooms + .filter((r) => { + if (filter.generic_search_term) { + return r.name.toLowerCase().includes(filter.generic_search_term.toLowerCase()); + } + + // Today only one room type is supported (https://spec.matrix.org/v1.15/client-server-api/#types) + // TODO: https://rocketchat.atlassian.net/browse/FDR-152 -> Implement logic to handle custom room types + // if (filter.room_types) { + // } + + return true; + }) + .map((room) => ({ + ...defaultObj, + ...room, + })), + }, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts new file mode 100644 index 0000000000000..e8ee53c4b964d --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -0,0 +1,250 @@ +import type { HomeserverServices, EventID } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const UsernameSchema = { + type: 'string', + pattern: '^@[A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix user ID in format @user:server.com', +}; + +const RoomIdSchema = { + type: 'string', + pattern: '^![A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix room ID in format !room:server.com', +}; + +const EventIdSchema = { + type: 'string', + pattern: '^\\$[A-Za-z0-9_=\\/.+-]+(:(.+))?$', + description: 'Matrix event ID in format $event', +}; + +const TimestampSchema = { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', +}; + +const DepthSchema = { + type: 'number', + minimum: 0, + description: 'Event depth', +}; + +const ServerNameSchema = { + type: 'string', + description: 'Matrix server name', +}; + +const SendJoinParamsSchema = { + type: 'object', + properties: { + roomId: RoomIdSchema, + stateKey: EventIdSchema, + }, + required: ['roomId', 'stateKey'], +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema); + +const EventHashSchema = { + type: 'object', + properties: { + sha256: { + type: 'string', + description: 'SHA256 hash of the event', + }, + }, + required: ['sha256'], +}; + +const EventSignatureSchema = { + type: 'object', + description: 'Event signatures by server and key ID', +}; + +const MembershipEventContentSchema = { + type: 'object', + properties: { + membership: { + type: 'string', + enum: ['join', 'leave', 'invite', 'ban', 'knock'], + description: 'Membership state', + }, + displayname: { + type: 'string', + nullable: true, + }, + avatar_url: { + type: 'string', + nullable: true, + }, + join_authorised_via_users_server: { + type: 'string', + nullable: true, + }, + is_direct: { + type: 'boolean', + nullable: true, + }, + reason: { + type: 'string', + description: 'Reason for membership change', + nullable: true, + }, + }, + required: ['membership'], +}; + +const EventBaseSchema = { + type: 'object', + properties: { + type: { + type: 'string', + description: 'Event type', + }, + content: { + type: 'object', + description: 'Event content', + }, + sender: UsernameSchema, + room_id: RoomIdSchema, + origin_server_ts: TimestampSchema, + depth: DepthSchema, + prev_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Previous events in the room', + }, + auth_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Authorization events', + }, + origin: { + type: 'string', + description: 'Origin server', + }, + hashes: { + ...EventHashSchema, + nullable: true, + }, + signatures: { + ...EventSignatureSchema, + nullable: true, + }, + unsigned: { + type: 'object', + description: 'Unsigned data', + nullable: true, + }, + }, + required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events', 'origin'], +}; + +const SendJoinEventSchema = { + type: 'object', + allOf: [ + EventBaseSchema, + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'm.room.member', + }, + content: { + type: 'object', + allOf: [ + MembershipEventContentSchema, + { + type: 'object', + properties: { + membership: { + type: 'string', + const: 'join', + }, + }, + required: ['membership'], + }, + ], + }, + state_key: UsernameSchema, + }, + required: ['type', 'content', 'state_key'], + }, + ], +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isSendJoinEventProps = ajv.compile(SendJoinEventSchema); + +const SendJoinResponseSchema = { + type: 'object', + properties: { + event: { + type: 'object', + description: 'The processed join event', + }, + state: { + type: 'array', + items: { + type: 'object', + }, + description: 'Current state events in the room', + }, + auth_chain: { + type: 'array', + items: { + type: 'object', + }, + description: 'Authorization chain for the event', + }, + members_omitted: { + type: 'boolean', + description: 'Whether member events were omitted', + }, + origin: ServerNameSchema, + }, + required: ['event', 'state', 'auth_chain', 'members_omitted', 'origin'], +}; + +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema); + +export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { + const { sendJoin } = services; + + return new Router('/federation').put( + '/v2/send_join/:roomId/:stateKey', + { + params: isSendJoinParamsProps, + body: isSendJoinEventProps, + response: { + 200: isSendJoinResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId, stateKey } = c.req.param(); + const body = await c.req.json(); + + const response = await sendJoin.sendJoin(roomId, stateKey as EventID, body); + + return { + body: response, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts new file mode 100644 index 0000000000000..ffbdd639811e0 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -0,0 +1,401 @@ +import type { HomeserverServices, EventID } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { canAccessEvent } from '../middlewares'; + +const SendTransactionParamsSchema = { + type: 'object', + properties: { + txnId: { + type: 'string', + description: 'Transaction ID', + }, + }, + required: ['txnId'], +}; + +const isSendTransactionParamsProps = ajv.compile(SendTransactionParamsSchema); + +const GetEventParamsSchema = { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'Event ID', + }, + }, + required: ['eventId'], + additionalProperties: false, +}; + +const isGetEventParamsProps = ajv.compile(GetEventParamsSchema); + +const GetEventResponseSchema = { + type: 'object', + properties: { + origin_server_ts: { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', + }, + origin: { + type: 'string', + description: 'Origin server', + }, + pdus: { + type: 'array', + items: { + type: 'object', + }, + description: 'Persistent data units (PDUs)', + }, + }, + required: ['origin_server_ts', 'origin', 'pdus'], +}; + +const isGetEventResponseProps = ajv.compile(GetEventResponseSchema); + +const EventHashSchema = { + type: 'object', + properties: { + sha256: { + type: 'string', + description: 'SHA256 hash of the event', + }, + }, + required: ['sha256'], +}; + +const EventSignatureSchema = { + type: 'object', + description: 'Event signatures by server and key ID', +}; + +const EventBaseSchema = { + type: 'object', + properties: { + type: { + type: 'string', + description: 'Event type', + }, + content: { + type: 'object', + description: 'Event content', + }, + sender: { + type: 'string', + pattern: '^@[A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix user ID in format @user:server.com', + }, + room_id: { + type: 'string', + pattern: '^![A-Za-z0-9_=\\/.+-]+:(.+)$', + description: 'Matrix room ID in format !room:server.com', + }, + origin_server_ts: { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', + }, + depth: { + type: 'number', + minimum: 0, + description: 'Event depth', + }, + prev_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Previous events in the room', + }, + auth_events: { + type: 'array', + items: { + type: 'string', + }, + description: 'Authorization events', + }, + origin: { + type: 'string', + description: 'Origin server', + }, + hashes: { + ...EventHashSchema, + nullable: true, + }, + signatures: { + ...EventSignatureSchema, + nullable: true, + }, + unsigned: { + type: 'object', + description: 'Unsigned data', + nullable: true, + }, + }, + required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events'], +}; + +const SendTransactionBodySchema = { + type: 'object', + properties: { + origin: { + type: 'string', + description: 'Origin server', + }, + origin_server_ts: { + type: 'number', + minimum: 0, + description: 'Unix timestamp in milliseconds', + }, + pdus: { + type: 'array', + items: EventBaseSchema, + description: 'Persistent data units (PDUs) to process', + default: [], + }, + edus: { + type: 'array', + items: { + type: 'object', + additionalProperties: true, + }, + description: 'Ephemeral data units (EDUs)', + default: [], + nullable: true, + }, + }, + required: ['origin', 'origin_server_ts', 'pdus'], +}; + +const isSendTransactionBodyProps = ajv.compile(SendTransactionBodySchema); + +const SendTransactionResponseSchema = { + type: 'object', + properties: { + pdus: { + type: 'object', + description: 'Processing results for each PDU', + }, + edus: { + type: 'object', + description: 'Processing results for each EDU', + }, + }, + required: ['pdus', 'edus'], +}; + +const isSendTransactionResponseProps = ajv.compile(SendTransactionResponseSchema); + +const ErrorResponseSchema = { + type: 'object', + properties: { + error: { + type: 'string', + }, + details: { + type: 'object', + }, + }, + required: ['error', 'details'], +}; + +const isErrorResponseProps = ajv.compile(ErrorResponseSchema); + +const GetStateIdsParamsSchema = { + type: 'object', + properties: { + event_id: { + type: 'string', + }, + }, + required: ['event_id'], +}; + +const isGetStateIdsParamsProps = ajv.compile(GetStateIdsParamsSchema); + +const GetStateIdsResponseSchema = { + type: 'object', + properties: { + stateIds: { + type: 'array', + items: { + type: 'string', + }, + }, + }, +}; + +const isGetStateIdsResponseProps = ajv.compile(GetStateIdsResponseSchema); +const GetStateParamsSchema = { + type: 'object', + properties: { + event_id: { + type: 'string', + }, + }, +}; +const isGetStateParamsProps = ajv.compile<{ + event_id: string; +}>(GetStateParamsSchema); + +const GetStateResponseSchema = { + type: 'object', + properties: { + state: { + type: 'object', + }, + }, +}; + +const isGetStateResponseProps = ajv.compile(GetStateResponseSchema); + +export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { + const { event, federationAuth } = services; + + // PUT /_matrix/federation/v1/send/{txnId} + return ( + new Router('/federation') + .put( + '/v1/send/:txnId', + { + params: isSendTransactionParamsProps, + body: isSendTransactionBodyProps, + response: { + 200: isSendTransactionResponseProps, + 400: isErrorResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const body = await c.req.json(); + + try { + await event.processIncomingTransaction(body); + } catch (error: any) { + // TODO custom error types? + if (error.message === 'too-many-concurrent-transactions') { + return { + statusCode: 429, + body: { + errorcode: 'M_UNKNOWN', + error: 'Too many concurrent transactions', + }, + }; + } + + return { + statusCode: 400, + body: {}, + }; + } + + return { + body: { + pdus: {}, + edus: {}, + }, + statusCode: 200, + }; + }, + ) + + // GET /_matrix/federation/v1/state_ids/{roomId} + + .get( + '/v1/state_ids/:roomId', + { + params: isGetStateIdsParamsProps, + response: { + 200: isGetStateIdsResponseProps, + }, + }, + async (c) => { + const roomId = c.req.param('roomId'); + const eventId = c.req.query('event_id'); + + if (!eventId) { + return { + body: { + errcode: 'M_NOT_FOUND', + error: 'Event not found', + }, + statusCode: 404, + }; + } + + const stateIds = await event.getStateIds(roomId, eventId as EventID); + + return { + body: stateIds, + statusCode: 200, + }; + }, + ) + .get( + '/v1/state/:roomId', + { + params: isGetStateParamsProps, + response: { + 200: isGetStateResponseProps, + }, + }, + async (c) => { + const roomId = c.req.param('roomId'); + const eventId = c.req.query('event_id'); + + if (!eventId) { + return { + body: { + errcode: 'M_NOT_FOUND', + error: 'Event not found', + }, + statusCode: 404, + }; + } + const state = await event.getState(roomId, eventId as EventID); + return { + statusCode: 200, + body: state, + }; + }, + ) + // GET /_matrix/federation/v1/event/{eventId} + .get( + '/v1/event/:eventId', + { + params: isGetEventParamsProps, + response: { + 200: isGetEventResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + canAccessEvent(federationAuth), + async (c) => { + const eventData = await event.getEventById(c.req.param('eventId') as EventID); + if (!eventData) { + return { + body: { + errcode: 'M_NOT_FOUND', + error: 'Event not found', + }, + statusCode: 404, + }; + } + + return { + body: { + origin_server_ts: eventData.event.origin_server_ts, + origin: eventData.origin, + pdus: [eventData.event], + }, + statusCode: 200, + }; + }, + ) + ); +}; diff --git a/ee/packages/federation-matrix/src/api/_matrix/versions.ts b/ee/packages/federation-matrix/src/api/_matrix/versions.ts new file mode 100644 index 0000000000000..684b3c14035aa --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -0,0 +1,54 @@ +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +const GetVersionsResponseSchema = { + type: 'object', + properties: { + server: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Server software name', + }, + version: { + type: 'string', + description: 'Server software version', + }, + }, + required: ['name', 'version'], + }, + }, + required: ['server'], +}; + +const isGetVersionsResponseProps = ajv.compile(GetVersionsResponseSchema); + +export const getFederationVersionsRoutes = (services: HomeserverServices) => { + const { config } = services; + + return new Router('/federation').get( + '/v1/version', + { + response: { + 200: isGetVersionsResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async () => { + const response = { + server: { + name: config.serverName, + version: config.version, + }, + }; + + return { + body: response, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/middlewares.ts b/ee/packages/federation-matrix/src/api/middlewares.ts new file mode 100644 index 0000000000000..1b1f9579d201c --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares.ts @@ -0,0 +1,61 @@ +import { errCodes } from '@rocket.chat/federation-sdk'; +import type { EventAuthorizationService, EventID } from '@rocket.chat/federation-sdk'; +import type { Context, Next } from 'hono'; + +export const canAccessMedia = (federationAuth: EventAuthorizationService) => async (c: Context, next: Next) => { + try { + const url = new URL(c.req.url); + const path = url.search ? `${c.req.path}${url.search}` : c.req.path; + + const verificationResult = await federationAuth.canAccessMediaFromAuthorizationHeader( + c.req.param('mediaId'), + c.req.header('Authorization') || '', + c.req.method, + path, + undefined, + ); + + if (!verificationResult.authorized) { + return c.json( + { + errcode: errCodes[verificationResult.errorCode].errcode, + error: errCodes[verificationResult.errorCode].error, + }, + errCodes[verificationResult.errorCode].status, + ); + } + + return next(); + } catch (error) { + return c.json(errCodes.M_UNKNOWN, 500); + } +}; + +export const canAccessEvent = (federationAuth: EventAuthorizationService) => async (c: Context, next: Next) => { + try { + const url = new URL(c.req.url); + const path = url.search ? `${c.req.path}${url.search}` : c.req.path; + + const verificationResult = await federationAuth.canAccessEventFromAuthorizationHeader( + c.req.param('eventId') as EventID, + c.req.header('Authorization') || '', + c.req.method, + path, + undefined, + ); + + if (!verificationResult.authorized) { + return c.json( + { + errcode: errCodes[verificationResult.errorCode].errcode, + error: errCodes[verificationResult.errorCode].error, + }, + errCodes[verificationResult.errorCode].status, + ); + } + + return next(); + } catch (error) { + return c.json(errCodes.M_UNKNOWN, 500); + } +}; diff --git a/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts b/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts new file mode 100644 index 0000000000000..78c145b695e84 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts @@ -0,0 +1,61 @@ +import { Settings } from '@rocket.chat/core-services'; +import { createMiddleware } from 'hono/factory'; +import mem from 'mem'; + +// cache for 60 seconds +const getAllowList = mem( + async () => { + const allowListSetting = await Settings.get('Federation_Service_Allow_List'); + return allowListSetting + ? allowListSetting + .split(',') + .map((d) => d.trim().toLowerCase()) + .filter(Boolean) + : null; + }, + { maxAge: 60000 }, +); + +/** + * Parses all key-value pairs from a Matrix authorization header. + * Example: X-Matrix origin="matrix.org", key="value", ... + * Returns an object with all parsed values. + */ +// TODO make this function more of a utility if needed elsewhere +function parseMatrixAuthorizationHeader(header: string): Record { + const result: Record = {}; + // Match key="value" pairs + const regex = /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/g; + let match; + while ((match = regex.exec(header)) !== null) { + result[match[1]] = match[2]; + } + return result; +} + +export const isFederationDomainAllowedMiddleware = createMiddleware(async (c, next) => { + const allowList = await getAllowList(); + if (!allowList || allowList.length === 0) { + // No restriction, allow all + return next(); + } + + // Extract all key-value pairs from Matrix authorization header + const authHeader = c.req.header('authorization'); + if (!authHeader) { + return c.json({ errcode: 'M_UNAUTHORIZED', error: 'Missing Authorization headers.' }, 401); + } + + const authValues = parseMatrixAuthorizationHeader(authHeader); + const domain = authValues.origin?.toLowerCase(); + if (!domain) { + return c.json({ errcode: 'M_MISSING_ORIGIN', error: 'Missing origin in authorization header.' }, 401); + } + + // Check if domain is in allowed list + if (allowList.some((allowed) => domain.endsWith(allowed))) { + return next(); + } + + return c.json({ errcode: 'M_FORBIDDEN', error: 'Federation from this domain is not allowed.' }, 403); +}); diff --git a/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts b/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts new file mode 100644 index 0000000000000..692130bbdc938 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts @@ -0,0 +1,9 @@ +import { Settings } from '@rocket.chat/core-services'; +import { createMiddleware } from 'hono/factory'; + +export const isFederationEnabledMiddleware = createMiddleware(async (c, next) => { + if (!(await Settings.get('Federation_Service_Enabled'))) { + return c.json({ error: 'Federation is not enabled' }, 403); + } + return next(); +}); diff --git a/ee/packages/federation-matrix/src/api/middlewares/isLicenseEnabled.ts b/ee/packages/federation-matrix/src/api/middlewares/isLicenseEnabled.ts new file mode 100644 index 0000000000000..86947598892fe --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares/isLicenseEnabled.ts @@ -0,0 +1,9 @@ +import { License } from '@rocket.chat/core-services'; +import { createMiddleware } from 'hono/factory'; + +export const isLicenseEnabledMiddleware = createMiddleware(async (c, next) => { + if (!(await License.hasModule('federation'))) { + return c.json({ error: 'Federation is not enabled' }, 403); + } + return next(); +}); diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts new file mode 100644 index 0000000000000..563ae4d404f1c --- /dev/null +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -0,0 +1,89 @@ +import { api } 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 { Logger } from '@rocket.chat/logger'; +import { Rooms, Users } from '@rocket.chat/models'; + +const logger = new Logger('federation-matrix:edu'); + +export const edus = async (emitter: Emitter, eduProcessTypes: { typing: boolean; presence: boolean }) => { + emitter.on('homeserver.matrix.typing', async (data) => { + if (!eduProcessTypes.typing) { + return; + } + + try { + const matrixRoom = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } }); + if (!matrixRoom) { + logger.debug(`No bridged room found for Matrix room_id: ${data.room_id}`); + return; + } + + const matrixUser = await Users.findOne({ 'federation.mui': data.user_id }); + if (!matrixUser?.username) { + logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); + return; + } + + void api.broadcast('user.activity', { + user: matrixUser.username, + isTyping: data.typing, + roomId: matrixRoom._id, + }); + } catch (error) { + logger.error('Error handling Matrix typing event:', error); + } + }); + + emitter.on('homeserver.matrix.presence', async (data) => { + if (!eduProcessTypes.presence) { + return; + } + + try { + const matrixUser = await Users.findOne({ 'federation.mui': data.user_id }); + if (!matrixUser) { + logger.debug(`No federated user found for Matrix user_id: ${data.user_id}`); + return; + } + + if (!matrixUser.federated) { + logger.debug(`User ${matrixUser.username} is not federated, skipping presence update from Matrix`); + return; + } + + const statusMap = { + online: UserStatus.ONLINE, + offline: UserStatus.OFFLINE, + unavailable: UserStatus.AWAY, + }; + + const status = statusMap[data.presence] || UserStatus.OFFLINE; + + if (matrixUser.status === status) { + logger.debug(`User ${matrixUser.username} already has status ${status}, skipping update`); + return; + } + + await Users.updateOne( + { _id: matrixUser._id }, + { + $set: { + status, + statusDefault: status, + }, + }, + ); + + const { _id, username, statusText, roles, name } = matrixUser; + void api.broadcast('presence.status', { + user: { status, _id, username, statusText, roles, name }, + previousStatus: undefined, + }); + logger.debug(`Updated presence for user ${matrixUser._id} to ${status} from Matrix federation`); + } catch (error) { + logger.error('Error handling Matrix presence event:', error); + } + }); +}; diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts new file mode 100644 index 0000000000000..9cf3abd0a9539 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -0,0 +1,22 @@ +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; + +import { edus } from './edu'; +import { member } from './member'; +import { message } from './message'; +import { ping } from './ping'; +import { reaction } from './reaction'; +import { room } from './room'; + +export function registerEvents( + emitter: Emitter, + serverName: string, + eduProcessTypes: { typing: boolean; presence: boolean }, +) { + ping(emitter); + message(emitter, serverName); + reaction(emitter); + member(emitter); + edus(emitter, eduProcessTypes); + room(emitter); +} diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts new file mode 100644 index 0000000000000..efa6c865aad0d --- /dev/null +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -0,0 +1,103 @@ +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 { Logger } from '@rocket.chat/logger'; +import { Rooms, Users } from '@rocket.chat/models'; + +const logger = new Logger('federation-matrix:member'); + +async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) { + const room = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } }); + if (!room) { + logger.warn(`No bridged room found for Matrix room_id: ${data.room_id}`); + return; + } + + // state_key is the user affected by the membership change + const affectedUser = await Users.findOne({ 'federation.mui': data.state_key }); + if (!affectedUser) { + logger.error(`No Rocket.Chat user found for bridged user: ${data.state_key}`); + return; + } + + // Check if this is a kick (sender != state_key) or voluntary leave (sender == state_key) + if (data.sender === data.state_key) { + // Voluntary leave + await Room.removeUserFromRoom(room._id, affectedUser); + 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 }); + + await Room.removeUserFromRoom(room._id, affectedUser, { + byUser: kickerUser || { _id: 'matrix.federation', username: 'Matrix User' }, + }); + + const reasonText = data.content.reason ? ` Reason: ${data.content.reason}` : ''; + logger.info(`User ${affectedUser.username} was kicked from room ${room._id} by ${data.sender} via Matrix federation.${reasonText}`); + } +} + +async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) { + 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); + if (localUser) { + 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 user = await Users.findOneById(insertedId); + if (!user) { + console.warn(`User with ID ${insertedId} not found after insertion`); + return; + } + await Room.addUserToRoom(room._id, user); +} + +export function member(emitter: Emitter) { + emitter.on('homeserver.matrix.membership', async (data) => { + try { + if (data.content.membership === 'leave') { + return membershipLeaveAction(data); + } + + if (data.content.membership === 'join') { + return membershipJoinAction(data); + } + + logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); + } catch (error) { + logger.error('Failed to process Matrix membership event:', error); + } + }); +} diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts new file mode 100644 index 0000000000000..93dd91856d766 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -0,0 +1,293 @@ +import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; +import type { IUser, IRoom, FileAttachmentProps } from '@rocket.chat/core-typings'; +import type { Emitter } from '@rocket.chat/emitter'; +import type { FileMessageType, MessageType, FileMessageContent, HomeserverEventSignatures, EventID } from '@rocket.chat/federation-sdk'; +import { Logger } from '@rocket.chat/logger'; +import { Users, Rooms, Messages } from '@rocket.chat/models'; + +import { fileTypes } from '../FederationMatrix'; +import { toInternalMessageFormat, toInternalQuoteMessageFormat } from '../helpers/message.parsers'; +import { MatrixMediaService } from '../services/MatrixMediaService'; + +const logger = new Logger('federation-matrix:message'); + +async function getThreadMessageId(threadRootEventId: EventID): Promise<{ tmid: string; tshow: boolean } | undefined> { + const threadRootMessage = await Messages.findOneByFederationId(threadRootEventId); + if (!threadRootMessage) { + logger.warn('Thread root message not found for event:', threadRootEventId); + return; + } + + const shouldSetTshow = !threadRootMessage?.tcount; + return { tmid: threadRootMessage._id, tshow: shouldSetTshow }; +} + +async function handleMediaMessage( + url: string, + fileInfo: FileMessageContent['info'], + msgtype: MessageType, + messageBody: string, + user: IUser, + room: IRoom, + matrixRoomId: string, + eventId: EventID, + tmid?: string, +): Promise<{ + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + tmid?: string; + attachments: [FileAttachmentProps]; +}> { + const mimeType = fileInfo?.mimetype; + const fileName = messageBody; + + const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(url, matrixRoomId, { + name: messageBody, + size: fileInfo?.size, + type: mimeType, + roomId: room._id, + userId: user._id, + }); + + let fileExtension = ''; + if (fileName?.includes('.')) { + fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; + } else if (mimeType?.includes('/')) { + fileExtension = mimeType.split('/')[1] || ''; + if (fileExtension === 'jpeg') { + fileExtension = 'jpg'; + } + } + + const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`; + + let attachment: FileAttachmentProps = { + title: fileName, + type: 'file', + title_link: fileUrl, + title_link_download: true, + description: '', + }; + + if (msgtype === 'm.image') { + attachment = { + ...attachment, + image_url: fileUrl, + image_type: mimeType, + image_size: fileInfo?.size || 0, + ...(fileInfo?.w && + fileInfo?.h && { + image_dimensions: { + width: fileInfo.w, + height: fileInfo.h, + }, + }), + }; + } else if (msgtype === 'm.video') { + attachment = { + ...attachment, + video_url: fileUrl, + video_type: mimeType, + video_size: fileInfo?.size || 0, + }; + } else if (msgtype === 'm.audio') { + attachment = { + ...attachment, + audio_url: fileUrl, + audio_type: mimeType, + audio_size: fileInfo?.size || 0, + }; + } + + return { + fromId: user._id, + rid: room._id, + msg: '', + federation_event_id: eventId, + tmid, + attachments: [attachment], + }; +} + +export function message(emitter: Emitter, serverName: string) { + emitter.on('homeserver.matrix.message', async (data) => { + try { + const { content } = data; + const { msgtype } = content; + const messageBody = content.body.toString(); + + if (!messageBody && !msgtype) { + logger.debug('No message content found in event'); + return; + } + + // at this point we know for sure the user already exists + const user = await Users.findOne({ 'federation.mui': data.sender }); + if (!user) { + throw new Error(`User not found for sender: ${data.sender}`); + } + + const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); + if (!room) { + throw new Error(`No mapped room found for room_id: ${data.room_id}`); + } + + const relation = content['m.relates_to']; + + const isThreadMessage = relation && relation.rel_type === 'm.thread'; + const threadRootEventId = isThreadMessage && relation.event_id; + + const quoteMessageEventId = relation && 'm.in_reply_to' in relation && relation['m.in_reply_to']?.event_id; + + const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined; + + const isEditedMessage = relation?.rel_type === 'm.replace'; + if (isEditedMessage && relation?.event_id && data.content['m.new_content']) { + logger.debug('Received edited message from Matrix, updating existing message'); + const originalMessage = await Messages.findOneByFederationId(relation.event_id); + if (!originalMessage) { + logger.error('Original message not found for edit:', relation.event_id); + return; + } + if (originalMessage.federation?.eventId !== relation.event_id) { + return; + } + if (originalMessage.msg === data.content['m.new_content']?.body) { + logger.debug('No changes in message content, skipping update'); + return; + } + + if (quoteMessageEventId && room.name) { + const messageToReplyToUrl = await MeteorService.getMessageURLToReplyTo( + room.t as string, + room._id, + room.name, + originalMessage._id, + ); + const formatted = await toInternalQuoteMessageFormat({ + messageToReplyToUrl, + formattedMessage: data.content.formatted_body || '', + rawMessage: messageBody, + homeServerDomain: serverName, + senderExternalId: data.sender, + }); + await Message.updateMessage( + { + ...originalMessage, + msg: formatted, + }, + user, + originalMessage, + ); + return; + } + + const formatted = toInternalMessageFormat({ + rawMessage: data.content['m.new_content'].body, + formattedMessage: data.content.formatted_body || '', + homeServerDomain: serverName, + senderExternalId: data.sender, + }); + await Message.updateMessage( + { + ...originalMessage, + msg: formatted, + }, + user, + originalMessage, + ); + return; + } + + if (quoteMessageEventId && room.name) { + const originalMessage = await Messages.findOneByFederationId(quoteMessageEventId); + if (!originalMessage) { + logger.error('Original message not found for quote:', quoteMessageEventId); + return; + } + const messageToReplyToUrl = await MeteorService.getMessageURLToReplyTo(room.t as string, room._id, room.name, originalMessage._id); + const formatted = await toInternalQuoteMessageFormat({ + messageToReplyToUrl, + formattedMessage: data.content.formatted_body || '', + rawMessage: messageBody, + homeServerDomain: serverName, + senderExternalId: data.sender, + }); + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: room._id, + msg: formatted, + federation_event_id: data.event_id, + thread, + }); + return; + } + + const isMediaMessage = Object.values(fileTypes).includes(msgtype as FileMessageType); + if (isMediaMessage && content.url) { + const result = await handleMediaMessage( + content.url, + content.info, + msgtype, + messageBody, + user, + room, + data.room_id, + data.event_id, + thread?.tmid, + ); + await Message.saveMessageFromFederation(result); + } else { + const formatted = toInternalMessageFormat({ + rawMessage: messageBody, + formattedMessage: data.content.formatted_body || '', + homeServerDomain: serverName, + senderExternalId: data.sender, + }); + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: room._id, + msg: formatted, + federation_event_id: data.event_id, + thread, + }); + } + } catch (error) { + logger.error('Error processing Matrix message:', error); + } + }); + + emitter.on('homeserver.matrix.redaction', async (data) => { + try { + const redactedEventId = data.redacts; + if (!redactedEventId) { + logger.debug('No redacts field in redaction event'); + return; + } + + const messageEvent = await FederationMatrix.getEventById(redactedEventId); + if (!messageEvent || messageEvent.type !== 'm.room.message') { + logger.debug(`Event ${redactedEventId} is not a message event`); + return; + } + + const rcMessage = await Messages.findOneByFederationId(data.redacts); + if (!rcMessage) { + logger.debug(`No RC message found for event ${data.redacts}`); + return; + } + const internalUsername = data.sender; + const user = await Users.findOneByUsername(internalUsername); + if (!user) { + logger.debug(`User not found: ${internalUsername}`); + return; + } + + await Message.deleteMessage(user, rcMessage); + } catch (error) { + logger.error('Failed to process Matrix removal redaction:', error); + } + }); +} diff --git a/ee/packages/federation-matrix/src/events/ping.ts b/ee/packages/federation-matrix/src/events/ping.ts new file mode 100644 index 0000000000000..3bcd05d042431 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/ping.ts @@ -0,0 +1,8 @@ +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; + +export const ping = async (emitter: Emitter) => { + emitter.on('homeserver.ping', async (data) => { + console.log('Message received from homeserver', data); + }); +}; diff --git a/ee/packages/federation-matrix/src/events/reaction.ts b/ee/packages/federation-matrix/src/events/reaction.ts new file mode 100644 index 0000000000000..e9d9c402864f1 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/reaction.ts @@ -0,0 +1,93 @@ +import { Message, FederationMatrix } from '@rocket.chat/core-services'; +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; +import { Logger } from '@rocket.chat/logger'; +import { Users, Messages } from '@rocket.chat/models'; // Rooms +import emojione from 'emojione'; + +const logger = new Logger('federation-matrix:reaction'); + +export function reaction(emitter: Emitter) { + emitter.on('homeserver.matrix.reaction', async (data) => { + try { + const isSetReaction = data.content?.['m.relates_to']; + + const reactionTargetEventId = isSetReaction?.event_id; + const reactionKey = isSetReaction?.key; + + const [userPart, domain] = data.sender.split(':'); + if (!userPart || !domain) { + logger.error('Invalid Matrix sender ID format:', data.sender); + return; + } + + const internalUsername = data.sender; + const user = await Users.findOneByUsername(internalUsername); + if (!user) { + logger.error(`No RC user mapping found for Matrix event ${reactionTargetEventId} ${internalUsername}`); + return; + } + + if (!isSetReaction) { + logger.debug(`No relates_to content in reaction event`); + return; + } + + const rcMessage = await Messages.findOneByFederationId(reactionTargetEventId); + if (!rcMessage) { + logger.debug(`No RC message mapping found for Matrix event ${reactionTargetEventId}`); + return; + } + + const reactionEmoji = emojione.toShort(reactionKey); + await Message.reactToMessage(user._id, reactionEmoji, rcMessage._id, true); + await Messages.setFederationReactionEventId(internalUsername, rcMessage._id, reactionEmoji, data.event_id); + } catch (error) { + logger.error('Failed to process Matrix reaction:', error); + } + }); + + emitter.on('homeserver.matrix.redaction', async (data) => { + try { + const redactedEventId = data.redacts; + if (!redactedEventId) { + logger.debug('No redacts field in redaction event'); + return; + } + + const reactionEvent = await FederationMatrix.getEventById(redactedEventId); + if (!reactionEvent || reactionEvent.type !== 'm.reaction') { + logger.debug(`Event ${redactedEventId} is not a reaction event`); + return; + } + + const reactionContent = reactionEvent.content?.['m.relates_to']; + if (!reactionContent) { + logger.debug('No relates_to content in reaction event'); + return; + } + + const targetMessageEventId = reactionContent.event_id; + const reactionKey = reactionContent.key; + + const rcMessage = await Messages.findOneByFederationId(targetMessageEventId); + if (!rcMessage) { + logger.debug(`No RC message found for event ${targetMessageEventId}`); + return; + } + + const internalUsername = data.sender; + const user = await Users.findOneByUsername(internalUsername); + if (!user) { + logger.debug(`User not found: ${internalUsername}`); + return; + } + + const reactionEmoji = emojione.toShort(reactionKey); + await Message.reactToMessage(user._id, reactionEmoji, rcMessage._id, false); + await Messages.unsetFederationReactionEventId(redactedEventId, rcMessage._id, reactionEmoji); + } catch (error) { + logger.error('Failed to process Matrix reaction redaction:', error); + } + }); +} diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts new file mode 100644 index 0000000000000..00deac2ad2419 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -0,0 +1,59 @@ +import { Room } from '@rocket.chat/core-services'; +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; +import { Rooms, Users } from '@rocket.chat/models'; + +export function room(emitter: Emitter) { + emitter.on('homeserver.matrix.room.name', async (data) => { + const { room_id: roomId, name, user_id: userId } = data; + + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + await Room.saveRoomName(localRoomId._id, localUserId._id, name); + }); + + emitter.on('homeserver.matrix.room.topic', async (data) => { + const { room_id: roomId, topic, user_id: userId } = data; + + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + await Room.saveRoomTopic(localRoomId._id, topic, { _id: localUserId._id, username: userId }); + }); + + emitter.on('homeserver.matrix.room.role', async (data) => { + const { room_id: roomId, user_id: userId, sender_id: senderId, role } = data; + + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + const localSenderId = await Users.findOne({ 'federation.mui': senderId }, { projection: { _id: 1 } }); + if (!localSenderId) { + throw new Error('mapped user not found'); + } + + await Room.addUserRoleRoomScoped(localSenderId._id, localUserId._id, localRoomId._id, role); + }); +} diff --git a/ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts b/ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts new file mode 100644 index 0000000000000..7076a8f472310 --- /dev/null +++ b/ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts @@ -0,0 +1,1732 @@ +import { + toExternalMessageFormat, + toExternalQuoteMessageFormat, + toInternalMessageFormat, + toInternalQuoteMessageFormat, +} from './message.parsers'; + +describe('Federation - Infrastructure - Matrix - RocketTextParser', () => { + describe('#toInternalMessageFormat ()', () => { + it('should parse the user mention correctly', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: 'hey User Real Name', + formattedMessage: 'hey User Real Name', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey @user:server.com'); + }); + it('should parse the mentions correctly when there is some room mention in RC format', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: "hello @marcos.defendi:tests-b.fed.rocket.chat, here's from Server A, @all, @marcos.defendi:tests-b.fed.rocket.chat", + formattedMessage: + '

hello @marcos.defendi:tests-b.fed.rocket.chat, here's from Server A, !nAWjvnrjAoUWVMpqTy:tests-b.fed.rocket.chat, @marcos.defendi:tests-b.fed.rocket.chat

', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe("hello @marcos.defendi:tests-b.fed.rocket.chat, here's from Server A, @all, @marcos.defendi:tests-b.fed.rocket.chat"); + }); + it('should parse the mentions correctly when there is some room mention in Element format', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: "hello marcos.defendi, here's from Server A, #test-thread:matrix.org, marcos.defendi", + formattedMessage: + 'hello marcos.defendi, here\'s from Server A, #test-thread:matrix.org, marcos.defendi', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe("hello @marcos.defendi:tests-b.fed.rocket.chat, here's from Server A, @all, @marcos.defendi:tests-b.fed.rocket.chat"); + }); + + it('should parse the user mention correctly when using the RC format', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: '@user:localDomain.com @user', + formattedMessage: + '@user:localDomain.com @user:externalDomain.com', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('@user @user:externalDomain.com'); + }); + + it('should parse the user multiple mentions correctly when using the RC format', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: '@user @user:localDomain.com @user @user:localDomain.com', + formattedMessage: + '@user:externalDomain.com @user:localDomain.com @user:externalDomain.com @user:localDomain.com', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('@user:externalDomain.com @user @user:externalDomain.com @user'); + }); + + it('should parse the @all mention correctly', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: 'hey externalRoomId', + formattedMessage: 'hey externalRoomId', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey @all'); + }); + + it('should parse the @here mention correctly', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: 'hey externalRoomId', + formattedMessage: 'hey externalRoomId', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey @all'); + }); + + it('should parse the @user mention without to include the server name when the user is original from the local ', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: 'hey User Real Name', + formattedMessage: 'hey User Real Name', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey @user'); + }); + + it('should return the message as-is when it does not have any mention', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: 'hey people, how are you?', + formattedMessage: 'hey people, how are you?', + homeServerDomain: 'localDomain', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey people, how are you?'); + }); + + it('should parse the message with all the mentions correctly when an user has the same real name', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: + 'hey User Real Name, hey Remote User Real Name, hey Remote User Real Name, how are you? Hope **you** __are__ doing well', + formattedMessage: + '

hey User Real Name, hey Remote User Real Name, hey Remote User Real Name how are you? Hope you are doing well', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('hey @user, hey @remoteuser1:matrix.org, hey @remoteuser2:matrix.org, how are you? Hope **you** __are__ doing well'); + }); + + it('should parse correctly a message containing both local mentions + some markdown', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + formattedMessage: + '

hey User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing both external mentions + some markdown', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + formattedMessage: + '

hey, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing both local mentions + external mentions + some markdown', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + formattedMessage: + '

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `, + formattedMessage: + '

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet`); + }); + + it('should parse correctly a message containing mentions for the user himself + external mentions', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `@user, hello Remote User Real Name, here's @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `, + formattedMessage: + '

@user:externalDomain.com, hello Remote User Real Name, here\'s @user:externalDomain.com, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`@user:externalDomain.com, hello @remoteuser:matrix.org, here's @user:externalDomain.com, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet`); + }); + + it('should parse correctly a message containing both mentions', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: '@user, @user:matrix.org', + formattedMessage: + '

@user:externalDomain.com, @user:matrix.org', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('@user:externalDomain.com, @user:matrix.org'); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message + an email inside the message', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com + `, + formattedMessage: + '

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet marcos.defendi@email.com 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com`); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + + `, + formattedMessage: + '

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 `Inline code` ```typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); ``` 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\``); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec + emojis', async () => { + expect( + await toInternalMessageFormat({ + rawMessage: `hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀 + `, + formattedMessage: + '

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 `Inline code` ```typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); ``` 😀 😀 😀 😀 
', + homeServerDomain: 'localDomain.com', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀`); + }); + }); + + describe('#toInternalQuoteMessageFormat ()', () => { + const homeServerDomain = 'localDomain.com'; + const quotedMessage = `
In reply to originalEventSender
`; + it('should parse the external quote to the internal one correctly', async () => { + const rawMessage = '> <@originalEventSender:localDomain.com> Quoted message\n\n hey people, how are you?'; + const formattedMessage = `${quotedMessage}hey people, how are you?`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey people, how are you?`); + }); + + it('should parse the user mention correctly', async () => { + const rawMessage = '> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name'; + const formattedMessage = `${quotedMessage}hey User Real Name`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('[ ](http://localhost:3000/group/1?msg=2354543564) hey @user:server.com'); + }); + + it('should parse the nested quotes correctly', async () => { + const rawMessage = '> <@marcos.defendi:tests-a.fed.rocket.chat>\n> test\nhello nested quote'; + const nested = + '
In reply to originalEventSender
test
hello nested quote'; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage: nested, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('[ ](http://localhost:3000/group/1?msg=2354543564) hello nested quote'); + }); + + it('should parse the @all mention correctly', async () => { + const rawMessage = '> <@originalEventSender:localDomain.com> Quoted message\n\n hey externalRoomId'; + const formattedMessage = `${quotedMessage}hey externalRoomId`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('[ ](http://localhost:3000/group/1?msg=2354543564) hey @all'); + }); + + it('should parse the message with all the mentions correctly when an user has the same real name', async () => { + const rawMessage = + '> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, hey Remote User Real Name, hey Remote User Real Name, how are you? Hope **you** __are__ doing well'; + const formattedMessage = `${quotedMessage}

hey User Real Name, hey Remote User Real Name, hey Remote User Real Name how are you? Hope you are doing well`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe( + `[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, hey @remoteuser1:matrix.org, hey @remoteuser2:matrix.org, how are you? Hope **you** __are__ doing well`, + ); + }); + + it('should parse correctly a message containing both local mentions + some markdown', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + const formattedMessage = `${quotedMessage}

hey User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing both external mentions + some markdown', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + const formattedMessage = `${quotedMessage}

hey, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing mentions for the user himself + external mentions', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n @user, hello Remote User Real Name, here's @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `; + const formattedMessage = `${quotedMessage}

@user:externalDomain.com, hello Remote User Real Name, here\'s @user:externalDomain.com, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
`; + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) @user:externalDomain.com, hello @remoteuser:matrix.org, here's @user:externalDomain.com, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet`); + }); + + it('should parse correctly a message containing both mentions', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n @user, @user:matrix.org`; + const formattedMessage = `${quotedMessage}

@user:externalDomain.com, @user:matrix.org`; + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ).toBe('[ ](http://localhost:3000/group/1?msg=2354543564) @user:externalDomain.com, @user:matrix.org'); + }); + + it('should parse correctly a message containing both local mentions + external mentions + some markdown', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + const formattedMessage = `${quotedMessage}

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `; + const formattedMessage = `${quotedMessage}

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet`); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message + an email inside the message', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com + `; + const formattedMessage = `${quotedMessage}

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet marcos.defendi@email.com 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com`); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + `; + const formattedMessage = `${quotedMessage}

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 \`Inline code\` \`\`\`typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); \`\`\` 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\``); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec + emojis', async () => { + const rawMessage = `> <@originalEventSender:localDomain.com> Quoted message\n\n hey User Real Name, here its Remote User Real Name, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀 + `; + const formattedMessage = `${quotedMessage}

hey User Real Name, here its Remote User Real Name, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 \`Inline code\` \`\`\`typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); \`\`\` 😀 😀 😀 😀 
`; + + expect( + await toInternalQuoteMessageFormat({ + homeServerDomain, + rawMessage, + formattedMessage, + messageToReplyToUrl: 'http://localhost:3000/group/1?msg=2354543564', + senderExternalId: '@user:externalDomain.com', + }), + ) + .toBe(`[ ](http://localhost:3000/group/1?msg=2354543564) hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀`); + }); + }); +}); + +describe('Federation - Infrastructure - Matrix - MatrixTextParser', () => { + describe('#toExternalMessageFormat ()', () => { + it('should parse the user external mention correctly', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey @user:server.com', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain', + }), + ).toBe('

hey @user:server.com

'); + }); + + it('should parse the mentions correctly when using the RC format', async () => { + expect( + await toExternalMessageFormat({ + message: '@user:server.com @user', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

@user:server.com @user:localDomain.com

', + ); + }); + + it('should parse the multiple mentions correctly when using the RC format', async () => { + expect( + await toExternalMessageFormat({ + message: '@user @user:server.com @user @user:server.com', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

@user:localDomain.com @user:server.com @user:localDomain.com @user:server.com

', + ); + }); + + it('should parse the @all mention correctly', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey @all', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain', + }), + ).toBe('

hey externalRoomId

'); + }); + + it('should parse the @here mention correctly', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey @here', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain', + }), + ).toBe('

hey externalRoomId

'); + }); + + it('should parse the user local mentions appending the local domain server in the mention', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey @user', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe('

hey @user:localDomain.com

'); + }); + + it('should parse multiple and different mentions in the same message correctly', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey @user:server.com, hey @all, hey @here @user', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:server.com, hey externalRoomId, hey externalRoomId @user:localDomain.com

', + ); + }); + + it('should return the message as-is when it does not have any mention', async () => { + expect( + await toExternalMessageFormat({ + message: 'hey people, how are you?', + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe('

hey people, how are you?

'); + }); + + it('should parse correctly a message containing both local mentions + some markdown', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + ); + }); + + it('should parse correctly a message containing both external mentions + some markdown', async () => { + expect( + await toExternalMessageFormat({ + message: `hey, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + ); + }); + + it('should parse correctly a message containing both local mentions + external mentions + some markdown', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
', + ); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
', + ); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message + an email inside the message', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com + `, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet marcos.defendi@email.com 
', + ); + }); + + it('should parse correctly a message containing a mention in the beginning of the string + an email', async () => { + expect( + await toExternalMessageFormat({ + message: `@user, hello @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com + `, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

@user:localDomain.com, hello @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet marcos.defendi@email.com 
', + ); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + + `, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 `Inline code` ```typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); ``` 
', + ); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec + emojis', async () => { + expect( + await toExternalMessageFormat({ + message: `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀 + `, + externalRoomId: 'externalRoomId', + homeServerDomain: 'localDomain.com', + }), + ).toBe( + '

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 `Inline code` ```typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); ``` 😀 😀 😀 😀 
', + ); + }); + }); + + describe('#toExternalQuoteMessageFormat ()', () => { + const eventToReplyTo = 'eventToReplyTo'; + const externalRoomId = 'externalRoomId'; + const originalEventSender = 'originalEventSenderId'; + const homeServerDomain = 'localDomain.com'; + const quotedMessage = `
In reply to ${originalEventSender}
`; + + it('should parse the internal quote to the external one correctly', async () => { + const message = 'hey people, how are you?'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

${message}

`, + }); + }); + + it('should parse the external user mention correctly', async () => { + const message = 'hey @user:server.com'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:server.com

`, + }); + }); + + it('should parse the @all mention correctly', async () => { + const message = 'hey @all'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey externalRoomId

`, + }); + }); + + it('should parse the @here mention correctly', async () => { + const message = 'hey @here'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey externalRoomId

`, + }); + }); + + it('should parse the user local mentions appending the local domain server in the mention', async () => { + const message = 'hey @user'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com

`, + }); + }); + + it('should parse multiple and different mentions in the same message correctly', async () => { + const message = 'hey @user:server.com, hey @all, hey @here @user'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:server.com, hey externalRoomId, hey externalRoomId @user:localDomain.com

`, + }); + }); + + it('should return the message as-is when it does not have any mention', async () => { + const message = 'hey people, how are you?'; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

${message}

`, + }); + }); + + it('should parse correctly a message containing both local mentions + some markdown', async () => { + const message = `hey @user, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`, + }); + }); + + it('should parse correctly a message containing both external mentions + some markdown', async () => { + const message = `hey, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`, + }); + }); + + it('should parse correctly a message containing both local mentions + external mentions + some markdown', async () => { + const message = `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item`; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item 
`, + }); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message', async () => { + const message = `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + `; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet 
`, + }); + }); + + it('should parse correctly a message containing both mentions + some quoting inside the message + an email inside the message', async () => { + const message = `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see the list: + # List 1: + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet + + marcos.defendi@email.com + `; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see the list: # List 1: Ordered List

 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet marcos.defendi@email.com 
`, + }); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec', async () => { + const message = `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + + `; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 \`Inline code\` \`\`\`typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); \`\`\` 
`, + }); + }); + + it('should parse correctly a message containing a message with mentions + the whole markdown spec + emojis', async () => { + const message = `hey @user, here its @remoteuser:matrix.org, how are you? Hope **you** __are__ doing well, please see: + # Heading 1 + + **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ## Heading 2 + + _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + ### Heading 3 + + - Lists, Links and elements + + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + + **Ordered List** + + 1. List Item + 2. List Item + 3. List Item + 4. List Item + + > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. + + **Links:** + + [Google](google.com) + [Rocket.Chat](rocket.chat) + [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) + marcos.defendi@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? : text); + \`\`\` + 😀 + 😀 + 😀 + 😀 + `; + + expect( + await toExternalQuoteMessageFormat({ + eventToReplyTo, + message, + externalRoomId, + homeServerDomain, + originalEventSender, + }), + ).toEqual({ + message: `> <${originalEventSender}> \n\n${message}`, + formattedMessage: `${quotedMessage}

hey @user:localDomain.com, here its @remoteuser:matrix.org, how are you? Hope you are doing well, please see: # Heading 1

 **Paragraph text**: **Bold** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ## Heading 2 _Italict Text_: _Italict_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. ### Heading 3 - Lists, Links and elements **Unordered List** - List Item 1 - List Item 2 - List Item 3 - List Item 4 **Ordered List** 1. List Item 2. List Item 3. List Item 4. List Item > Quote test: **Bold** _Italic_ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales, enim et facilisis commodo, est augue venenatis ligula, in convallis erat felis nec nisi. In eleifend ligula a nunc efficitur, ut finibus enim fringilla. **Links:** [Google](google.com) [Rocket.Chat](rocket.chat) [Rocket.Chat Link Test](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [**Rocket.Chat Link Test**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [~~Rocket.Chat Link Test~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__Rocket.Chat Link Test__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) [__**~~Rocket.Chat Link Test~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351) marcos.defendi@rocket.chat +55991999999 \`Inline code\` \`\`\`typescript const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, variant: 'inline' | 'inlineWithoutBreaks' | 'document' = 'inline', ): ReactNode => (list?.includes(key) ? <MarkdownText parseEmoji variant={variant} content={text} /> : text); \`\`\` 😀 😀 😀 😀 
`, + }); + }); + }); +}); diff --git a/ee/packages/federation-matrix/src/helpers/message.parsers.ts b/ee/packages/federation-matrix/src/helpers/message.parsers.ts new file mode 100644 index 0000000000000..d971d98a747e3 --- /dev/null +++ b/ee/packages/federation-matrix/src/helpers/message.parsers.ts @@ -0,0 +1,255 @@ +import type { MentionPill as MentionPillType } from '@vector-im/matrix-bot-sdk'; +import { marked } from 'marked'; +import sanitizeHtml from 'sanitize-html'; +import type { IFrame } from 'sanitize-html'; + +interface IInternalMention { + mention: string; + realName: string; +} + +const DEFAULT_LINK_FOR_MATRIX_MENTIONS = 'https://matrix.to/#/'; +const DEFAULT_TAGS_FOR_MATRIX_QUOTES = ['mx-reply', 'blockquote']; +const INTERNAL_MENTIONS_FOR_EXTERNAL_USERS_REGEX = /@([0-9a-zA-Z-_.]+(@([0-9a-zA-Z-_.]+))?):+([0-9a-zA-Z-_.]+)(?=[^<>]*(?:<\w|$))/gm; // @username:server.com excluding any tags +const INTERNAL_MENTIONS_FOR_INTERNAL_USERS_REGEX = /(?:^|(?<=\s))@([0-9a-zA-Z-_.]+(@([0-9a-zA-Z-_.]+))?)(?=[^<>]*(?:<\w|$))/gm; // @username, @username.name excluding any tags and emails +const INTERNAL_GENERAL_REGEX = /(@all)|(@here)/gm; + +const getAllMentionsWithTheirRealNames = (message: string, homeServerDomain: string, senderExternalId: string): IInternalMention[] => { + const mentions: IInternalMention[] = []; + sanitizeHtml(message, { + allowedTags: ['a'], + exclusiveFilter: (frame: IFrame): boolean => { + const { + attribs: { href = '' }, + tag, + text, + } = frame; + const validATag = tag === 'a' && href && text; + if (!validATag) { + return false; + } + const isUsernameMention = href.includes(DEFAULT_LINK_FOR_MATRIX_MENTIONS) && href.includes('@'); + if (isUsernameMention) { + const [, username] = href.split('@'); + const [, serverDomain] = username.split(':'); + + const withoutServerIdentification = `@${username.split(':').shift()}`; + const fullUsername = `@${username}`; + const isMentioningHimself = senderExternalId === text; + + mentions.push({ + mention: serverDomain === homeServerDomain ? withoutServerIdentification : fullUsername, + realName: isMentioningHimself ? withoutServerIdentification : text, + }); + } + const isMentioningAll = href.includes(DEFAULT_LINK_FOR_MATRIX_MENTIONS) && !href.includes('@'); + if (isMentioningAll) { + mentions.push({ + mention: '@all', + realName: text, + }); + } + return false; + }, + }); + + return mentions; +}; + +export const toInternalMessageFormat = ({ + rawMessage, + formattedMessage, + homeServerDomain, + senderExternalId, +}: { + rawMessage: string; + formattedMessage: string; + homeServerDomain: string; + senderExternalId: string; +}): string => + replaceAllMentionsOneByOneSequentially( + rawMessage, + getAllMentionsWithTheirRealNames(formattedMessage, homeServerDomain, senderExternalId), + ); + +const MATCH_ANYTHING = 'w'; +const replaceAllMentionsOneByOneSequentially = (message: string, allMentionsWithRealNames: IInternalMention[]): string => { + let parsedMessage = ''; + let toCompareAgain = message; + + if (allMentionsWithRealNames.length === 0) { + return message; + } + + allMentionsWithRealNames.forEach(({ mention, realName }, mentionsIndex) => { + const negativeLookAhead = `(?!${MATCH_ANYTHING})`; + const realNameRegex = new RegExp(`(?') { + break; + } + splitLineIndex += 1; + } + + return splitLines.splice(splitLineIndex).join('\n').trim(); +} + +export const toInternalQuoteMessageFormat = async ({ + homeServerDomain, + formattedMessage, + rawMessage, + messageToReplyToUrl, + senderExternalId, +}: { + messageToReplyToUrl: string; + formattedMessage: string; + rawMessage: string; + homeServerDomain: string; + senderExternalId: string; +}): Promise => { + const withMentionsOnly = sanitizeHtml(formattedMessage, { + allowedTags: ['a'], + allowedAttributes: { + a: ['href'], + }, + nonTextTags: DEFAULT_TAGS_FOR_MATRIX_QUOTES, + }); + const rawMessageWithoutMatrixQuotingFormatting = stripReplyQuote(rawMessage); + + return `[ ](${messageToReplyToUrl}) ${replaceAllMentionsOneByOneSequentially( + rawMessageWithoutMatrixQuotingFormatting, + getAllMentionsWithTheirRealNames(withMentionsOnly, homeServerDomain, senderExternalId), + )}`; +}; + +const replaceMessageMentions = async ( + message: string, + mentionRegex: RegExp, + parseMatchFn: (match: string) => Promise, +): Promise => { + const promises: Promise[] = []; + + message.replace(mentionRegex, (match: string): any => promises.push(parseMatchFn(match))); + + const mentions = await Promise.all(promises); + + return message.replace(mentionRegex, () => ` ${mentions.shift()?.html}`); +}; + +const replaceMentionsFromLocalExternalUsersForExternalFormat = async (message: string): Promise => { + const { MentionPill } = await import('@vector-im/matrix-bot-sdk'); + + return replaceMessageMentions(message, INTERNAL_MENTIONS_FOR_EXTERNAL_USERS_REGEX, (match: string) => + MentionPill.forUser(match.trimStart()), + ); +}; + +const replaceInternalUsersMentionsForExternalFormat = async (message: string, homeServerDomain: string): Promise => { + const { MentionPill } = await import('@vector-im/matrix-bot-sdk'); + + return replaceMessageMentions(message, INTERNAL_MENTIONS_FOR_INTERNAL_USERS_REGEX, (match: string) => + MentionPill.forUser(`${match.trimStart()}:${homeServerDomain}`), + ); +}; + +const replaceInternalGeneralMentionsForExternalFormat = async (message: string, externalRoomId: string): Promise => { + const { MentionPill } = await import('@vector-im/matrix-bot-sdk'); + + return replaceMessageMentions(message, INTERNAL_GENERAL_REGEX, () => MentionPill.forRoom(externalRoomId)); +}; + +const removeAllExtraBlankSpacesForASingleOne = (message: string): string => message.replace(/\s+/g, ' ').trim(); + +const replaceInternalWithExternalMentions = async (message: string, externalRoomId: string, homeServerDomain: string): Promise => + replaceInternalUsersMentionsForExternalFormat( + await replaceMentionsFromLocalExternalUsersForExternalFormat( + await replaceInternalGeneralMentionsForExternalFormat(message, externalRoomId), + ), + homeServerDomain, + ); + +const convertMarkdownToHTML = async (message: string): Promise => marked.parse(message); + +export const toExternalMessageFormat = async ({ + externalRoomId, + homeServerDomain, + message, +}: { + message: string; + externalRoomId: string; + homeServerDomain: string; +}): Promise => + removeAllExtraBlankSpacesForASingleOne( + await convertMarkdownToHTML((await replaceInternalWithExternalMentions(message, externalRoomId, homeServerDomain)).trim()), + ); + +export const toExternalQuoteMessageFormat = async ({ + message, + eventToReplyTo, + externalRoomId, + homeServerDomain, + originalEventSender, +}: { + externalRoomId: string; + eventToReplyTo: string; + originalEventSender: string; + message: string; + homeServerDomain: string; +}): Promise<{ message: string; formattedMessage: string }> => { + const { RichReply } = await import('@vector-im/matrix-bot-sdk'); + + const formattedMessage = await convertMarkdownToHTML(message); + const finalFormattedMessage = await convertMarkdownToHTML( + await toExternalMessageFormat({ + message, + externalRoomId, + homeServerDomain, + }), + ); + + const { formatted_body: formattedBody } = RichReply.createFor( + externalRoomId, + { event_id: eventToReplyTo, sender: originalEventSender }, + formattedMessage, + finalFormattedMessage, + ); + const { body } = RichReply.createFor( + externalRoomId, + { event_id: eventToReplyTo, sender: originalEventSender }, + message, + finalFormattedMessage, + ); + + return { + message: body, + formattedMessage: formattedBody, + }; +}; diff --git a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts new file mode 100644 index 0000000000000..e19273f9e9a50 --- /dev/null +++ b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts @@ -0,0 +1,151 @@ +import { Upload } from '@rocket.chat/core-services'; +import type { IUpload } from '@rocket.chat/core-typings'; +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; +import { Logger } from '@rocket.chat/logger'; +import { Uploads } from '@rocket.chat/models'; + +const logger = new Logger('federation-matrix:media-service'); + +export interface IRemoteFileReference { + name: string; + size: number; + type: string; + mxcUri: string; + serverName: string; + mediaId: string; +} + +export class MatrixMediaService { + private static homeserverServices: HomeserverServices; + + static setHomeserverServices(services: HomeserverServices): void { + this.homeserverServices = services; + } + + static generateMXCUri(fileId: string, serverName: string): string { + return `mxc://${serverName}/${fileId}`; + } + + static parseMXCUri(mxcUri: string): { serverName: string; mediaId: string } | null { + const match = mxcUri.match(/^mxc:\/\/([^/]+)\/(.+)$/); + if (!match) { + logger.error('Invalid MXC URI format', { mxcUri }); + return null; + } + return { + serverName: match[1], + mediaId: match[2], + }; + } + + static async prepareLocalFileForMatrix(fileId: string, serverName: string, matrixRoomId: string): Promise { + try { + const file = await Uploads.findOneById(fileId); + if (!file) { + logger.error(`File ${fileId} not found in database`); + throw new Error(`File ${fileId} not found`); + } + + if (file.federation?.mxcUri) { + return file.federation.mxcUri; + } + + const mxcUri = this.generateMXCUri(fileId, serverName); + + await Uploads.setFederationInfo(fileId, { + mrid: matrixRoomId, + mxcUri, + serverName, + mediaId: fileId, + }); + + return mxcUri; + } catch (error) { + logger.error('Error preparing file for Matrix:', error); + throw error; + } + } + + static async getLocalFileForMatrixNode(mediaId: string, serverName: string): Promise { + try { + let file = await Uploads.findByFederationMediaIdAndServerName(mediaId, serverName); + + if (!file) { + file = await Uploads.findOneById(mediaId); + } + + if (!file) { + return null; + } + + return file; + } catch (error) { + logger.error('Error retrieving local file:', error); + return null; + } + } + + static async downloadAndStoreRemoteFile( + mxcUri: string, + matrixRoomId: string, + metadata: { + name: string; + size?: number; + type?: string; + messageId?: string; + roomId?: string; + userId?: string; + }, + ): Promise { + try { + const parts = this.parseMXCUri(mxcUri); + if (!parts) { + logger.error('Invalid MXC URI format', { mxcUri }); + throw new Error('Invalid MXC URI'); + } + + const uploadAlreadyExists = await Uploads.findByFederationMediaIdAndServerName(parts.mediaId, parts.serverName); + if (uploadAlreadyExists) { + return uploadAlreadyExists._id; + } + + if (!this.homeserverServices) { + throw new Error('Homeserver services not initialized. Call setHomeserverServices first.'); + } + + const buffer = await this.homeserverServices.media.downloadFromRemoteServer(parts.serverName, parts.mediaId); + if (!buffer) { + throw new Error('Download from remote server returned null content.'); + } + + // TODO: Make uploadFile support Partial to avoid calling a DB update right after the upload to set the federation info + const uploadedFile = await Upload.uploadFile({ + userId: metadata.userId || 'federation', + buffer, + details: { + name: metadata.name || 'unnamed', + size: buffer.length, + type: metadata.type || 'application/octet-stream', + rid: metadata.roomId, + userId: metadata.userId || 'federation', + }, + }); + + await Uploads.setFederationInfo(uploadedFile._id, { + mxcUri, + mrid: matrixRoomId, + serverName: parts.serverName, + mediaId: parts.mediaId, + }); + + return uploadedFile._id; + } catch (error) { + logger.error('Error downloading and storing remote file:', error); + throw error; + } + } + + static async getLocalFileBuffer(file: IUpload): Promise { + return Upload.getFileBuffer({ file }); + } +} diff --git a/ee/packages/federation-matrix/src/types/ICallbacks.ts b/ee/packages/federation-matrix/src/types/ICallbacks.ts new file mode 100644 index 0000000000000..687950c0b25f1 --- /dev/null +++ b/ee/packages/federation-matrix/src/types/ICallbacks.ts @@ -0,0 +1,18 @@ +import type { IMessage, IUser } from '@rocket.chat/core-typings'; + +export interface ICallbackPriority { + HIGH: number; + MEDIUM: number; + LOW: number; +} + +export interface ICallbacks { + priority: ICallbackPriority; + add(hook: string, callback: (...args: any[]) => any, priority?: number, id?: string): void; + remove(hook: string, id: string): void; +} + +export interface IFederationCallbackHandlers { + afterSetReaction?: (message: IMessage, params: { user: IUser; reaction: string }) => Promise; + afterUnsetReaction?: (message: IMessage, params: { user: IUser; reaction: string; oldMessage: IMessage }) => Promise; +} diff --git a/ee/packages/federation-matrix/tsconfig.build.json b/ee/packages/federation-matrix/tsconfig.build.json new file mode 100644 index 0000000000000..9f4e58a941b1d --- /dev/null +++ b/ee/packages/federation-matrix/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "skipLibCheck": true, + "isolatedModules": true, + "allowJs": false, + "checkJs": false, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "../../../../**/*", + ] +} \ No newline at end of file diff --git a/ee/packages/federation-matrix/tsconfig.json b/ee/packages/federation-matrix/tsconfig.json new file mode 100644 index 0000000000000..a15c40e5f497f --- /dev/null +++ b/ee/packages/federation-matrix/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@rocket.chat/tsconfig/server.json", + "compilerOptions": { + "strictPropertyInitialization": false, + "skipLibCheck": true, + "experimentalDecorators": true, + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "files": ["./src/FederationMatrix.ts"] +} diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 273e939de8541..4e5a67b300fa1 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/http-router": "workspace:^", "@rocket.chat/icons": "^0.43.0", "@rocket.chat/media-signaling": "workspace:^", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/core-services/src/LocalBroker.ts b/packages/core-services/src/LocalBroker.ts index 454b4e24af3f9..436e7995a7f3a 100644 --- a/packages/core-services/src/LocalBroker.ts +++ b/packages/core-services/src/LocalBroker.ts @@ -71,6 +71,8 @@ export class LocalBroker implements IBroker { } instance.removeAllListeners(); await instance.stopped(); + + this.services.delete(namespace); } /** diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 09f7855884dfe..f6eb3d7291412 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -151,7 +151,7 @@ export type EventSignatures = { scope?: string; }): void; 'user.updateCustomStatus'(userStatus: Omit): void; - 'user.typing'(data: { user: Partial; isTyping: boolean; roomId: string }): void; + 'user.activity'(data: { user: string; isTyping: boolean; roomId: string }): void; 'user.video-conference'(data: { userId: IUser['_id']; action: string; diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index d8b4e6e671cdb..2d7e4b9a96081 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -10,6 +10,7 @@ import type { IBannerService } from './types/IBannerService'; import type { ICalendarService } from './types/ICalendarService'; import type { IDeviceManagementService } from './types/IDeviceManagementService'; import type { IEnterpriseSettings } from './types/IEnterpriseSettings'; +import type { IFederationMatrixService } from './types/IFederationMatrixService'; import type { IFederationService, IFederationServiceEE } from './types/IFederationService'; import type { IImportService } from './types/IImportService'; import type { ILDAPService } from './types/ILDAPService'; @@ -68,6 +69,8 @@ export { FederationConfigurationStatus, } from './types/IFederationService'; +export { IFederationMatrixService } from './types/IFederationMatrixService'; + export { ConversationData, AgentOverviewDataOptions, @@ -192,3 +195,5 @@ export const User = proxify('user'); // Calls without wait. Means that the service is optional and the result may be an error // of service/method not available export const EnterpriseSettings = proxify('ee-settings'); + +export const FederationMatrix = proxify('federation-matrix'); diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts new file mode 100644 index 0000000000000..893bca7be5f3c --- /dev/null +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -0,0 +1,30 @@ +import type { IMessage, IRoomFederated, IRoomNativeFederated, IUser } from '@rocket.chat/core-typings'; +import type { Router } from '@rocket.chat/http-router'; + +export interface IFederationMatrixService { + getAllRoutes(): { + matrix: Router<'/_matrix'>; + wellKnown: Router<'/.well-known'>; + }; + createRoom(room: IRoomFederated, owner: IUser, members: string[]): Promise<{ room_id: string; event_id: string }>; + ensureFederatedUsersExistLocally(members: string[]): Promise; + createDirectMessageRoom(room: IRoomFederated, members: IUser[], creatorId: IUser['_id']): Promise; + sendMessage(message: IMessage, room: IRoomFederated, user: IUser): Promise; + deleteMessage(matrixRoomId: string, message: IMessage, uid: string): Promise; + sendReaction(messageId: string, reaction: string, user: IUser): Promise; + removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; + getEventById(eventId: string): Promise; + leaveRoom(rid: IRoomFederated['_id'], user: IUser): Promise; + kickUser(room: IRoomNativeFederated, removedUser: IUser, userWhoRemoved: IUser): Promise; + updateMessage(room: IRoomNativeFederated, message: IMessage): Promise; + updateRoomName(rid: string, displayName: string, user: IUser): Promise; + updateRoomTopic(room: IRoomNativeFederated, topic: string, user: IUser): Promise; + addUserRoleRoomScoped( + room: IRoomNativeFederated, + senderId: string, + userId: string, + role: 'moderator' | 'owner' | 'leader' | 'user', + ): Promise; + inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: IUser): Promise; + notifyUserTyping(rid: string, user: string, isTyping: boolean): Promise; +} diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 29da139ef63c8..793781bcfe070 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -9,6 +9,25 @@ export interface IMessageService { user: Pick, extraData?: Partial, ): Promise; + saveMessageFromFederation({ + fromId, + rid, + msg, + federation_event_id, + file, + files, + attachments, + thread, + }: { + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + file?: IMessage['file']; + files?: IMessage['files']; + attachments?: IMessage['attachments']; + thread?: { tmid: string; tshow: boolean }; + }): Promise; saveSystemMessageAndNotifyUser( type: MessageTypesValues, rid: string, diff --git a/packages/core-services/src/types/IMeteor.ts b/packages/core-services/src/types/IMeteor.ts index 09f4e4470a20c..4d16cac1d781b 100644 --- a/packages/core-services/src/types/IMeteor.ts +++ b/packages/core-services/src/types/IMeteor.ts @@ -27,4 +27,5 @@ export interface IMeteor extends IServiceClass { }>; notifyGuestStatusChanged(token: string, status: string): Promise; getURL(path: string, params?: Record, cloudDeepLinkUrl?: string): Promise; + getMessageURLToReplyTo(roomType: string, roomId: string, roomName: string, messageIdToReplyTo: string): Promise; } diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 70d64dc9966f0..0bb6ae3e6a0d8 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -10,6 +10,7 @@ export interface ISubscriptionExtraData { export interface ICreateRoomOptions extends Partial> { creator: string; subscriptionExtra?: ISubscriptionExtraData; + federatedRoomId?: string; } export interface ICreateRoomExtraData extends Record { @@ -57,4 +58,6 @@ export interface IRoomService { beforeUserRemoved(room: IRoom): Promise; beforeNameChange(room: IRoom): Promise; beforeTopicChange(room: IRoom): Promise; + saveRoomName(roomId: string, userId: string, name: string): Promise; + addUserRoleRoomScoped(fromUserId: string, userId: string, roomId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise; } diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index d1dc67b00230c..33adf890dd3ba 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -215,6 +215,7 @@ export interface IMessage extends IRocketChatRecord { token?: string; federation?: { eventId: string; + version?: number; }; /* used when message type is "omnichannel_sla_change_history" */ @@ -237,6 +238,20 @@ export interface IMessage extends IRocketChatRecord { }; } +export type EncryptedMessageContent = { + content: { + algorithm: 'rc.v1.aes-sha2'; + ciphertext: string; + }; +}; + +export const isEncryptedMessageContent = (content: unknown): content is EncryptedMessageContent => + typeof content === 'object' && + content !== null && + 'content' in content && + typeof (content as any).content === 'object' && + (content as any).content?.algorithm === 'rc.v1.aes-sha2'; + export interface ISystemMessage extends IMessage { t: MessageTypesValues; } @@ -268,7 +283,7 @@ export interface IFederatedMessage extends IMessage { export interface INativeFederatedMessage extends IMessage { federation: { - version: `${number}`; + version: number; eventId: string; }; } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 4019597aefb05..629ac5ee1e124 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -102,14 +102,20 @@ export interface IRoomWithJoinCode extends IRoom { joinCode: string; } +declare const __brand: unique symbol; +type Brand = { [__brand]: B }; +export type Branded = T & Brand; + export interface IRoomFederated extends IRoom { + _id: Branded; federated: true; } -export interface IRoomNativeFederated extends IRoom { - federated: true; +export interface IRoomNativeFederated extends IRoomFederated { federation: { - version: `${number}`; + version: number; + // Matrix's room ID. Example: !XqJXqZxXqJXq:matrix.org + mrid: string; }; } diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 8bcabee2fa8de..e487151b94bf5 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -59,6 +59,12 @@ export interface IUpload { hashes?: { sha256: string; }; + federation?: { + mxcUri: string; + mrid: string; + serverName: string; + mediaId: string; + }; } export type IUploadWithUser = IUpload & { user?: Pick }; diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 5f961011f0af6..bd76e342b7a5d 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -227,7 +227,9 @@ export interface IUser extends IRocketChatRecord { federated?: boolean; // @deprecated federation?: { - version?: `${number}`; + version?: number; + mui?: string; + origin?: string; avatarUrl?: string; searchedServerNames?: string[]; }; @@ -258,17 +260,20 @@ export interface IRegisterUser extends IUser { } export const isRegisterUser = (user: IUser): user is IRegisterUser => user.username !== undefined && user.name !== undefined; + export const isUserFederated = (user: Partial | Partial>) => 'federated' in user && user.federated === true; export interface IUserNativeFederated extends IUser { federated: true; federation: { - version: `${number}`; + version: number; + mui: string; + origin: string; }; } export const isUserNativeFederated = (user: Partial): user is IUserNativeFederated => - isUserFederated(user) && 'federation' in user && typeof user.federation?.version === 'string'; + isUserFederated(user) && 'federation' in user && typeof user.federation?.version === 'number'; export type IUserDataEvent = { id: unknown; diff --git a/packages/core-typings/src/federation/IMatrixBridgedRoom.ts b/packages/core-typings/src/federation/IMatrixBridgedRoom.ts deleted file mode 100644 index 635ea94090dbf..0000000000000 --- a/packages/core-typings/src/federation/IMatrixBridgedRoom.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { IRocketChatRecord } from '../IRocketChatRecord'; - -export interface IMatrixBridgedRoom extends IRocketChatRecord { - rid: string; - mri: string; -} diff --git a/packages/core-typings/src/federation/IMatrixBridgedUser.ts b/packages/core-typings/src/federation/IMatrixBridgedUser.ts deleted file mode 100644 index dfc805158c86c..0000000000000 --- a/packages/core-typings/src/federation/IMatrixBridgedUser.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { IRocketChatRecord } from '../IRocketChatRecord'; - -export interface IMatrixBridgedUser extends IRocketChatRecord { - uid: string; - mui: string; - remote: boolean; - fromServer?: string; -} diff --git a/packages/core-typings/src/federation/index.ts b/packages/core-typings/src/federation/index.ts index f3dbfe7778c66..5b98253d935b4 100644 --- a/packages/core-typings/src/federation/index.ts +++ b/packages/core-typings/src/federation/index.ts @@ -1,4 +1 @@ -export * from './IMatrixBridgedRoom'; -export * from './IMatrixBridgedUser'; - export * from './v1'; diff --git a/packages/http-router/src/Router.ts b/packages/http-router/src/Router.ts index d6f795b8d76ea..040de9ed457b8 100644 --- a/packages/http-router/src/Router.ts +++ b/packages/http-router/src/Router.ts @@ -401,6 +401,14 @@ export class Router< ); return router; } + + getHonoRouter(): Hono<{ + Variables: { + remoteAddress: string; + }; + }> { + return this.innerRouter; + } } type Prettify = { diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 645ae30ca9ef2..07fbfdaeb58f0 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -1528,8 +1528,6 @@ "FEDERATION_Domain": "النطاق", "FEDERATION_Domain_Alert": "لا تغيره بعد تمكين الميزة، لا يمكننا معالجة تغييرات النطاق حتى الآن.", "FEDERATION_Domain_Description": "أضف النطاق الذي يجب أن يرتبط به هذا الخادم - مثل: ‎@rocket.chat.", - "FEDERATION_Enabled": "محاولة دمج دعم الاتحاد.", - "FEDERATION_Enabled_Alert": "دعم الاتحاد عمل مستمر. لا ينصح باستخدامه في نظام الإنتاج في الوقت الحالي.", "FEDERATION_Public_Key": "المفتاح العام", "FEDERATION_Public_Key_Description": "هذا هو المفتاح الذي تحتاج إلى مشاركته مع نظرائك.", "FEDERATION_Status": "الحالة", @@ -1562,7 +1560,6 @@ "Federation": "اتحاد", "Federation_Enable": "تمكين الاتحاد", "Federation_Matrix": "اتحاد", - "Federation_Matrix_Enabled_Alert": "دعم اتحاد المصفوفة بألفا. لا ينصح باستخدامه في نظام الإنتاج في الوقت الحالي. يمكن العثور على مزيد من المعلومات حول دعم اتحاد Matrix هنا ", "Federation_Matrix_as_token": "رمز AppService", "Federation_Matrix_enabled": "تم التمكين", "Federation_Matrix_homeserver_domain": "مجال الخادم الرئيسي", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 67f24306c31df..fe905e88bce6d 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -1517,8 +1517,6 @@ "FEDERATION_Domain": "Domini", "FEDERATION_Domain_Alert": "No canvieu això després d’habilitar la funció, encara no podem manejar els canvis de domini.", "FEDERATION_Domain_Description": "Afegiu el domini al qual ha d'estar vinculat aquest servidor, per exemple: @rocket.chat", - "FEDERATION_Enabled": "IIntenteu integrar el suport de la federació.", - "FEDERATION_Enabled_Alert": "La federació de suport està en progrés. El seu ús en un entorn de producció no es recomana de moment.", "FEDERATION_Public_Key": "Clau pública", "FEDERATION_Public_Key_Description": "Aquesta és la clau que necessita per compartir amb els seus companys.", "FEDERATION_Status": "Estat", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 3d507cba56c64..4e41c05bd0196 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -1282,8 +1282,6 @@ "FEDERATION_Domain": "Doména", "FEDERATION_Domain_Alert": "Neměňte po povolení této funkce, zatím nelze zpracovávat změny domény.", "FEDERATION_Domain_Description": "Přidejte doménu, na kterou by měl být tento server propojen - například: @rocket.chat.", - "FEDERATION_Enabled": "Pokus o integraci podpory Federace.", - "FEDERATION_Enabled_Alert": "Na podpoře Federace se stále pracuje. Použití v produkčním prostředí se v současné době nedoporučuje.", "FEDERATION_Public_Key": "Veřejný klíč", "FEDERATION_Public_Key_Description": "Toto je klíč, který musíte sdílet se svými partnery.", "FEDERATION_Status": "Stav", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 0781dca4860f6..9493767278d99 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -1359,8 +1359,6 @@ "FEDERATION_Domain": "Domæne", "FEDERATION_Domain_Alert": "Ændre ikke dette efter aktivering af funktionen. Vi kan ikke håndtere domæneændringer endnu.", "FEDERATION_Domain_Description": "Tilføj det domæne som denne server skal linkes til - for eksempel: @ rocket.chat.", - "FEDERATION_Enabled": "Forsøg på at integrere support for Federation.", - "FEDERATION_Enabled_Alert": "Support for Federation er ved at blive implementeret. Anvendelse på et produktionssystem anbefales ikke pt.", "FEDERATION_Public_Key": "Offentlig nøgle", "FEDERATION_Public_Key_Description": "Dette er den nøgle du skal dele med dine gruppe.", "FEDERATION_Status": "Status", diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 127ddfd8c9fd8..686ef55aa24ac 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -1257,8 +1257,6 @@ "FEDERATION_Domain": "Domain", "FEDERATION_Domain_Alert": "Nach dem Aktivieren dieser Funktion darf dieser Wert nicht geändert werden. Änderungen an der Domain können wir noch nicht verarbeiten.", "FEDERATION_Domain_Description": "Füge die Domäne hinzu, mit der dieser Server verlinkt werden soll - zum Beispiel: @ rocket.chat.", - "FEDERATION_Enabled": "Versuche, den Federation-Support zu integrieren. Um diesen Wert zu ändern, muss Rocket.Chat neu gestartet werden. ", - "FEDERATION_Enabled_Alert": "Federation-Support ist in Arbeit. Die Verwendung auf einem Produktionssystem wird derzeit nicht empfohlen.", "FEDERATION_Public_Key": "Öffentlicher Schüssel", "FEDERATION_Public_Key_Description": "Dies ist der Schlüssel, der mit den Peers geteilt werden muss.", "FEDERATION_Status": "Status", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index ddad785e1f013..ae3d7ba521b00 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -1685,8 +1685,6 @@ "FEDERATION_Domain": "Domain", "FEDERATION_Domain_Alert": "Nach dem Aktivieren dieser Funktion darf dieser Wert nicht geändert werden. Änderungen an der Domain können wir noch nicht verarbeiten.", "FEDERATION_Domain_Description": "Fügen Sie die Domäne hinzu, mit der dieser Server verlinkt werden soll - zum Beispiel: @rocket.chat.", - "FEDERATION_Enabled": "Versuch die Verbund-Unterstützung zu integrieren. ", - "FEDERATION_Enabled_Alert": "Verbund-Unterstützung ist in Arbeit. Die Verwendung auf einem Produktionssystem wird derzeit nicht empfohlen.", "FEDERATION_Public_Key": "Öffentlicher Schüssel", "FEDERATION_Public_Key_Description": "Dies ist der Schlüssel, den Sie mit Ihren Kollegen teilen müssen.", "FEDERATION_Status": "Status", @@ -1719,10 +1717,8 @@ "Features": "Funktionen", "Federated": "Verbunden", "Federation": "Verbund", - "Federation_Description": "Verbund ermöglicht es einer begrenzten Anzahl von Arbeitsbereichen, miteinander zu kommunizieren.", "Federation_Enable": "Verbund aktivieren", "Federation_Matrix": "Verbund V2", - "Federation_Matrix_Enabled_Alert": "Weitere Informationen zur Unterstützung von Matrix Verbund finden Sie hier (Nach jeder Konfiguration ist ein Neustart erforderlich, damit die Änderungen wirksam werden)", "Federation_Matrix_Federated": "Verbunden", "Federation_Matrix_Federated_Description": "Wenn Sie einen Verbundraum erstellen, können Sie weder Verschlüsselung noch Broadcasting aktivieren", "Federation_Matrix_Federated_Description_disabled": "Diese Funktion ist derzeit in diesem Arbeitsbereich deaktiviert.", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 54815d3ee34d3..0943a0dbec11a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2066,8 +2066,9 @@ "FEDERATION_Domain": "Domain", "FEDERATION_Domain_Alert": "Do not change this after enabling the feature, we can't handle domain changes yet.", "FEDERATION_Domain_Description": "Add the domain that this server should be linked to - for example: @rocket.chat.", - "FEDERATION_Enabled": "Attempt to integrate federation support.", - "FEDERATION_Enabled_Alert": "Federation Support is a work in progress. Use on a production system is not recommended at this time.", + "Old_Federation_Alert": "This Federation version is not supported anymore. Please configure the new alternative above named Native Federation.
More Information about Matrix Federation support can be found here", + "Rocket.Chat Federation": "Rocket.Chat Federation (NOT SUPPORTED)", + "Matrix Bridge": "Matrix Bridge (NOT SUPPORTED)", "FEDERATION_Public_Key": "Public Key", "FEDERATION_Public_Key_Description": "This is the key you need to share with your peers.", "FEDERATION_Status": "Status", @@ -2109,12 +2110,11 @@ "Features": "Features", "Federated": "Federated", "Federation": "Federation", - "Federation_Description": "Federation allows an unlimited number of workspaces to communicate with each other.", + "Federation_Description": "Federation allows remote workspaces to communicate with each other through the Matrix protocol.", "Federation_Enable": "Enable Federation", "Federation_Example_matrix_server": "Example: matrix.org", "Federation_Federated_room_search": "Federated room search", "Federation_Matrix": "Federation V2", - "Federation_Matrix_Enabled_Alert": "More Information about Matrix Federation support can be found here (After any configuration, a restart is required to the changes take effect)", "Federation_Matrix_Federated": "Federated", "Federation_Matrix_Federated_Description": "By creating a federated room you'll not be able to enable encryption nor broadcast", "Federation_Matrix_Federated_Description_disabled": "Federation is currently disabled on this workspace", @@ -2153,6 +2153,27 @@ "Federation_Search_federated_rooms": "Search federated rooms", "Federation_is_currently_disabled_on_this_workspace": "Federation is currently disabled on this workspace", "Federation_slash_commands": "Federation commands", + "Federation_Service_Enabled": "Enable native federation", + "Federation_Service_Enabled_Description": "Enable native federation for inter-server communication using Matrix Protocol.", + "Federation_Service_EDU_Process_Typing": "Process Typing events", + "Federation_Service_EDU_Process_Typing_Description": "Send and receive events of user typing a message between federated servers.", + "Federation_Service_EDU_Process_Typing_Alert": "Enabling typing events may increase the load on your server and network traffic considerably, especially if you have many users. Only enable this option if you understand the implications and have the necessary resources to handle the additional load.", + "Federation_Service_EDU_Process_Presence": "Process Presence events", + "Federation_Service_EDU_Process_Presence_Description": "Send and receive events of user presence (online, offline, etc.) between federated servers.", + "Federation_Service_EDU_Process_Presence_Alert": "Enabling presence events may increase the load on your server and network traffic considerably, especially if you have many users. Only enable this option if you understand the implications and have the necessary resources to handle the additional load.", + "Federation_Service_Alert": "This is an alfa feature not intended for production usage!
It may not be stable and/or performatic. Please be aware that it may change, break, or even be removed in the future without any notice.", + "Federation_Service_Domain": "Federated Domain", + "Federation_Service_Domain_Description": "The domain that this server should respond to, for example: `acme.com`. This will be used as the suffix for user IDs (e.g., `@user:acme.com`).
If your chat server is accessible from a different domain than the one you want to use for federation, you should follow our documentation to configure the `.well-known` file on your web server.", + "Federation_Service_Domain_Alert": "Inform only the domain, do not include http(s)://, slashes or any path after it.
Use something like `acme.com` and not `https://acme.com/chat`.", + "Federation_Service_Matrix_Signing_Algorithm": "Signing Key Algorithm", + "Federation_Service_Matrix_Signing_Version": "Signing Key Version", + "Federation_Service_Matrix_Signing_Key": "Signing Key", + "Federation_Service_Matrix_Signing_Key_Description": "The private base64 signing key used to authenticate federation requests. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated servers over Matrix protocol and should be kept confidential.", + "Federation_Service_max_allowed_size_of_public_rooms_to_join": "Maximum number of members when joining a public room in a remote server", + "Federation_Service_max_allowed_size_of_public_rooms_to_join_Alert": "Keep in mind, that the bigger the room you allow for users to join, the more time it will take to join that room, besides the amount of resource it will use.
Read more", + "Federation_Service_max_allowed_size_of_public_rooms_to_join_Description": "The user limit from a public room in a remote server that can still be joined. Rooms that exceed this setting will still be listed, but users won't be able to join them", + "Federation_Service_Allow_List": "Domain Allow List", + "Federation_Service_Allow_List_Description": "Restrict federation to the given allow list of domains.", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index b8f9168ccbf47..7ba662a09d66e 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -1554,8 +1554,6 @@ "FEDERATION_Domain": "Dominio", "FEDERATION_Domain_Alert": "No cambies esta opción después de habilitar la función; aún no podemos gestionar los cambios de dominio.", "FEDERATION_Domain_Description": "Añade el dominio al que debe estar vinculado este servidor; por ejemplo, @rocket.chat.", - "FEDERATION_Enabled": "Intenta integrar la compatibilidad con la federación.", - "FEDERATION_Enabled_Alert": "La función Compatibilidad de federación se está desarrollando. De momento, no se recomienda su uso en sistemas de producción.", "FEDERATION_Public_Key": "Clave pública", "FEDERATION_Public_Key_Description": "Esta es la clave que tienes que compartir con los puntos de conexión.", "FEDERATION_Status": "Estado", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 29cf74f2ceba8..936cec5bb41c6 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -1732,8 +1732,6 @@ "FEDERATION_Domain": "Toimialue", "FEDERATION_Domain_Alert": "Älä muuta tätä ominaisuuden käyttöönoton jälkeen, emme voi vielä käsitellä toimialuemuutoksia.", "FEDERATION_Domain_Description": "Lisää toimialue, johon tämä palvelin on tarkoitus liittää - esimerkiksi: @rocket.chat.", - "FEDERATION_Enabled": "Yritä integroida liittoutumisen tuki.", - "FEDERATION_Enabled_Alert": "Liittoutumisen tuki on kehitteillä. Käyttöä tuotantojärjestelmässä ei suositella tällä hetkellä.", "FEDERATION_Public_Key": "Julkinen avain", "FEDERATION_Public_Key_Description": "Tämä on avain, joka sinun on jaettava vertaistesi kanssa.", "FEDERATION_Status": "Tila", @@ -1766,10 +1764,8 @@ "Features": "Ominaisuudet", "Federated": "Liittoutunut", "Federation": "Liittoutuminen", - "Federation_Description": "Liittoutumisen ansiosta rajoittamaton määrä työtiloja voi olla yhteydessä keskenään.", "Federation_Enable": "Ota liittoutuminen käyttöön", "Federation_Matrix": "Liittoutuminen V2", - "Federation_Matrix_Enabled_Alert": "Lisätietoja Matrix Federation -tuesta on täällä (Muutokset on otettava voimaan uudelleenkäynnistyksellä aina määritysten jälkeen)", "Federation_Matrix_Federated": "Liittoutunut", "Federation_Matrix_Federated_Description": "Luomalla liittoutuneen huoneen et voi ottaa käyttöön salausta etkä lähetystä", "Federation_Matrix_Federated_Description_disabled": "Tämä ominaisuus on tällä hetkellä poistettu käytöstä tässä työtilassa.", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index f7dc8a7d0feb6..0e5b0e85292cc 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -1530,8 +1530,6 @@ "FEDERATION_Domain": "Domaine", "FEDERATION_Domain_Alert": "Ne changez pas ce paramètre après avoir activé la fonctionnalité, nous ne prenons pas encore en charge les changements de domaine.", "FEDERATION_Domain_Description": "Ajoutez le domaine auquel ce serveur doit être lié, par exemple : @rocket.chat.", - "FEDERATION_Enabled": "Tentative d'intégration de la prise en charge de la fédération.", - "FEDERATION_Enabled_Alert": "La prise en charge de la fédération est en cours d'intégration. L'utilisation sur un système de production n'est pas recommandée pour le moment.", "FEDERATION_Public_Key": "Clé publique", "FEDERATION_Public_Key_Description": "Il s'agit de la clé que vous devez partager avec vos pairs.", "FEDERATION_Status": "Statut", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 6a1f6207b25f8..8b48548e4dcb9 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -1799,8 +1799,6 @@ "FEDERATION_Domain": "कार्यक्षेत्र", "FEDERATION_Domain_Alert": "सुविधा सक्षम करने के बाद इसे न बदलें, हम अभी तक डोमेन परिवर्तनों को संभाल नहीं सकते हैं।", "FEDERATION_Domain_Description": "वह डोमेन जोड़ें जिससे यह सर्वर लिंक होना चाहिए - उदाहरण के लिए: @rocket.chat.", - "FEDERATION_Enabled": "फेडरेशन समर्थन को एकीकृत करने का प्रयास।", - "FEDERATION_Enabled_Alert": "फेडरेशन सपोर्ट का कार्य प्रगति पर है। इस समय उत्पादन प्रणाली पर उपयोग की अनुशंसा नहीं की जाती है।", "FEDERATION_Public_Key": "सार्वजनिक कुंजी", "FEDERATION_Public_Key_Description": "यह वह कुंजी है जिसे आपको अपने साथियों के साथ साझा करने की आवश्यकता है।", "FEDERATION_Status": "स्थिति", @@ -1835,12 +1833,10 @@ "Features": "विशेषताएँ", "Federated": "संघीय", "Federation": "फेडरेशन", - "Federation_Description": "फ़ेडरेशन असीमित संख्या में कार्यस्थानों को एक-दूसरे के साथ संचार करने की अनुमति देता है।", "Federation_Enable": "फ़ेडरेशन सक्षम करें", "Federation_Example_matrix_server": "उदाहरण: मैट्रिक्स.ऑर्ग", "Federation_Federated_room_search": "फ़ेडरेटेड कमरे की खोज", "Federation_Matrix": "फेडरेशन V2", - "Federation_Matrix_Enabled_Alert": "मैट्रिक्स फेडरेशन समर्थन के बारे में अधिक जानकारी यहां पाई जा सकती है (किसी भी कॉन्फ़िगरेशन के बाद, परिवर्तनों को प्रभावी करने के लिए पुनः आरंभ करना आवश्यक है)", "Federation_Matrix_Federated": "संघीय", "Federation_Matrix_Federated_Description": "फ़ेडरेटेड रूम बनाकर आप न तो एन्क्रिप्शन सक्षम कर पाएंगे और न ही प्रसारण", "Federation_Matrix_Federated_Description_disabled": "फ़ेडरेशन वर्तमान में इस कार्यक्षेत्र में अक्षम है.", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 89f5231e3a50e..9881471c5fdc1 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -1654,8 +1654,6 @@ "FEDERATION_Domain": "Tartomány", "FEDERATION_Domain_Alert": "Ne változtassa meg ezt a funkció engedélyezése után, mert még nem tudjuk kezelni a tartományváltoztatásokat.", "FEDERATION_Domain_Description": "A tartomány hozzáadása, amelyhez ezt a kiszolgálót hozzá kell kapcsolni – például: @rocket.chat.", - "FEDERATION_Enabled": "Kísérlet a föderációs támogatás integrálására.", - "FEDERATION_Enabled_Alert": "A föderáció támogatásának munkálatai folyamatban vannak. Egy produktív rendszeren történő használata jelenleg nem ajánlott.", "FEDERATION_Public_Key": "Nyilvános kulcs", "FEDERATION_Public_Key_Description": "Ez az a kulcs, amelyet meg kell osztania partnereivel.", "FEDERATION_Status": "Állapot", @@ -1688,10 +1686,8 @@ "Features": "Funkciók", "Federated": "Föderált", "Federation": "Föderáció", - "Federation_Description": "A föderáció lehetővé teszi, hogy korlátlan számú munkaterület kommunikáljon egymással.", "Federation_Enable": "Föderáció engedélyezése", "Federation_Matrix": "Föderáció V2", - "Federation_Matrix_Enabled_Alert": "Itt találhatók további információk a Matrix föderációs támogatásáról (Bármilyen beállítás után újraindítás szükséges a változtatások hatályba lépéséhez)", "Federation_Matrix_Federated": "Föderált", "Federation_Matrix_Federated_Description": "Egy föderált szoba létrehozásával nem fogja tudni engedélyezni sem a titkosítást, sem a műsorszórást", "Federation_Matrix_Federated_Description_disabled": "Ez a funkció jelenleg le van tiltva ezen a munkaterületen.", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 49db9fdab808f..427e6309f82a8 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -1513,8 +1513,6 @@ "FEDERATION_Domain": "ドメイン", "FEDERATION_Domain_Alert": "この機能を有効にした後はこれを変更しないでください。ドメイン変更はまだ処理できません。", "FEDERATION_Domain_Description": "このサーバーのリンク先のドメインを追加してください。例:@rocket.chat", - "FEDERATION_Enabled": "フェデレーションサポートを統合しようとしました。", - "FEDERATION_Enabled_Alert": "フェデレーションサポートは進行中の作業です。現時点では本番システムでの使用はお勧めできません。", "FEDERATION_Public_Key": "パブリックキー", "FEDERATION_Public_Key_Description": "これはピアと共有する必要があるキーです。", "FEDERATION_Status": "ステータス", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index f94708c1372df..d11670493e7d2 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -1218,8 +1218,6 @@ "FEDERATION_Domain": "დომენი", "FEDERATION_Domain_Alert": " არ შეცვალოთ ფუნქციის ჩართვის შემდეგ, ჩვენ ჯერ ვერ გაუმკლავდებით დომენის ცვლილებებს.", "FEDERATION_Domain_Description": "დაამატეთ დომენი, რომელთანაც უნდა იყოს დაკავშირებული ეს სერვერი - მაგალითად: @ rocket.chat.", - "FEDERATION_Enabled": "ფედერაციის მხარდაჭერის ინტეგრაციის მცდელობა.", - "FEDERATION_Enabled_Alert": "ფედერაციის მხარდაჭერაზე მიმდინარეობს მუშაობა. ამ დროისთვის არ არის რეკომენდებული საწარმოო სისტემაზე გამოყენება.", "FEDERATION_Public_Key": "საჯარო გასაღები", "FEDERATION_Public_Key_Description": "ეს არის გასაღები, რომელიც თქვენ უნდა გაუზიაროთ თანამშრომლებს(იმავე დონის)", "FEDERATION_Status": "სტატუსი", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index 79288107013a0..888bf2c4ab05f 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -1340,8 +1340,6 @@ "FEDERATION_Domain": "도메인", "FEDERATION_Domain_Alert": "기능을 활성화 한 후에는 변경하지 마십시오. 아직 도메인 변경을 처리 할 수 없습니다.", "FEDERATION_Domain_Description": "이 서버가 연결될 도메인을 추가하십시오 (예 : @ rocket.chat).", - "FEDERATION_Enabled": "Federation Support 통합을 시도하십시오.", - "FEDERATION_Enabled_Alert": "Federation Support가 처리된 작업입니다. 현재 프로덕션 시스템의 사용은 권장하지 않습니다.", "FEDERATION_Public_Key": "공개 키", "FEDERATION_Public_Key_Description": "이것은 Peer간 공유해야하는 키입니다.", "FEDERATION_Status": "상태", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 303a9a0bcc2ff..cc6acdde06822 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -2054,8 +2054,6 @@ "FEDERATION_Domain": "Domene", "FEDERATION_Domain_Alert": "Ikke endre dette etter at du har aktivert funksjonen, vi kan ikke håndtere domeneendringer ennå.", "FEDERATION_Domain_Description": "Legg til domenet som denne serveren skal kobles til - for eksempel: @rocket.chat.", - "FEDERATION_Enabled": "Forsøk på å integrere forbundsstøtte.", - "FEDERATION_Enabled_Alert": "Federation Support er et arbeid som pågår. Bruk på et produksjonssystem anbefales ikke på dette tidspunktet.", "FEDERATION_Public_Key": "Offentlig nøkkel", "FEDERATION_Public_Key_Description": "Dette er nøkkelen du trenger å dele med jevnaldrende.", "FEDERATION_Status": "Status", @@ -2097,12 +2095,10 @@ "Features": "Egenskaper", "Federated": "Forent", "Federation": "Føderasjon", - "Federation_Description": "Forening lar et ubegrenset antall arbeidsområder kommunisere med hverandre.", "Federation_Enable": "Aktiver Federation", "Federation_Example_matrix_server": "Eksempel: matrix.org", "Federation_Federated_room_search": "Søk forente rom", "Federation_Matrix": "Federation V2", - "Federation_Matrix_Enabled_Alert": "Mer informasjon om Matrix Federation-støtte finner du her (Etter enhver konfigurasjon kreves en omstart for at endringene trer i kraft)", "Federation_Matrix_Federated": "Forbundet", "Federation_Matrix_Federated_Description": "Ved å opprette et forent rom vil du ikke kunne aktivere kryptering eller kringkasting", "Federation_Matrix_Federated_Description_disabled": "Forening er for øyeblikket deaktivert for dette arbeidsområdet", diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index 399b1bf8eda1e..25a1ad267ea1e 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -1524,8 +1524,6 @@ "FEDERATION_Domain": "Domein", "FEDERATION_Domain_Alert": "Wijzig dit niet nadat u de functie hebt ingeschakeld, we kunnen nog geen domeinwijzigingen verwerken.", "FEDERATION_Domain_Description": "Voeg het domein toe waaraan deze server moet worden gekoppeld, bijvoorbeeld: @rocket.chat.", - "FEDERATION_Enabled": "Poging om federatieondersteuning te integreren.", - "FEDERATION_Enabled_Alert": "Federatie-ondersteuning is een werk in uitvoering. Gebruik op een productiesysteem wordt op dit moment niet aanbevolen.", "FEDERATION_Public_Key": "Publieke sleutel", "FEDERATION_Public_Key_Description": "Dit is de sleutel die je moet delen met je collega's.", "FEDERATION_Status": "Toestand", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 5c7abff2945ad..fcb2f57e8c719 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -1998,8 +1998,6 @@ "FEDERATION_Domain": "Domene", "FEDERATION_Domain_Alert": "Ikke endre dette etter at du har aktivert funksjonen, vi kan ikke håndtere domeneendringer ennå.", "FEDERATION_Domain_Description": "Legg til domenet som denne serveren skal kobles til - for eksempel: @rocket.chat.", - "FEDERATION_Enabled": "Forsøk på å integrere forbundsstøtte.", - "FEDERATION_Enabled_Alert": "Federation Support er et arbeid som pågår. Bruk på et produksjonssystem anbefales ikke på dette tidspunktet.", "FEDERATION_Public_Key": "Offentlig nøkkel", "FEDERATION_Public_Key_Description": "Dette er nøkkelen du trenger å dele med jevnaldrende.", "FEDERATION_Status": "Status", @@ -2041,12 +2039,10 @@ "Features": "Egenskaper", "Federated": "Forent", "Federation": "Føderasjon", - "Federation_Description": "Forening lar et ubegrenset antall arbeidsområder kommunisere med hverandre.", "Federation_Enable": "Aktiver Federation", "Federation_Example_matrix_server": "Eksempel: matrix.org", "Federation_Federated_room_search": "Søk forente rom", "Federation_Matrix": "Federation V2", - "Federation_Matrix_Enabled_Alert": "Mer informasjon om Matrix Federation-støtte finner du her (Etter enhver konfigurasjon kreves en omstart for at endringene trer i kraft)", "Federation_Matrix_Federated": "Forbundet", "Federation_Matrix_Federated_Description": "Ved å opprette et forent rom vil du ikke kunne aktivere kryptering eller kringkasting", "Federation_Matrix_Federated_Description_disabled": "Forening er for øyeblikket deaktivert for dette arbeidsområdet", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 339281b3a06f6..89b7a4c595dc9 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -1668,8 +1668,6 @@ "FEDERATION_Domain": "Domena", "FEDERATION_Domain_Alert": "Nie zmieniaj tego po włączeniu funkcji, nie obsługujemy jeszcze zmian w domenie.", "FEDERATION_Domain_Description": "Dodaj domenę, z którą ten serwer powinien być połączony - na przykład: @rocket.chat.", - "FEDERATION_Enabled": "Próba zintegrowania federation support.", - "FEDERATION_Enabled_Alert": "Federation support jest w trakcie realizacji. Użycie na systemie produkcyjnym nie jest w tym momencie zalecane.", "FEDERATION_Public_Key": "Klucz publiczny", "FEDERATION_Public_Key_Description": "To jest klucz, który musisz udostępnić swoim użytkownikom.", "FEDERATION_Status": "Status", @@ -1702,10 +1700,8 @@ "Features": "Ficzery", "Federated": "Sfederowany", "Federation": "Federacja", - "Federation_Description": "Federacja umożliwia komunikowanie się ze sobą nieograniczonej liczby obszarów roboczych.", "Federation_Enable": "Włącz Federację", "Federation_Matrix": "Federacja V2", - "Federation_Matrix_Enabled_Alert": "Wsparcie Federacji Matrix jest w wersji alfa. Stosowanie w systemie produkcyjnym nie jest obecnie zalecane.Więcej informacji na temat obsługi Matrix Federation można znaleźć tutaj", "Federation_Matrix_Federated": "Sfederowany", "Federation_Matrix_Federated_Description": "Tworząc pokój federacyjny nie będziesz mógł włączyć szyfrowania ani rozgłaszania", "Federation_Matrix_Federated_Description_disabled": "Ta funkcja jest obecnie wyłączona w tym obszarze roboczym.", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 0e65dcb74e179..863aaa2d5b43b 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -2033,8 +2033,6 @@ "FEDERATION_Domain": "Domínio", "FEDERATION_Domain_Alert": "Não altere isto depois de ativar o recurso, ainda não podemos lidar com as alterações de domínio.", "FEDERATION_Domain_Description": "Adicione o domínio a este servidor deve estar ligado por exemplo:", - "FEDERATION_Enabled": "Tentativa para a integração do suporte de federação.", - "FEDERATION_Enabled_Alert": "O suporte de federação é um trabalho em curso. Não recomendamos o uso num sistema de produção.", "FEDERATION_Public_Key": "Chave Pública", "FEDERATION_Public_Key_Description": "Esta é a chave que você deve compartilhar com outros peers.", "FEDERATION_Status": "Situação", @@ -2076,12 +2074,10 @@ "Features": "Funcionalidades", "Federated": "Federado", "Federation": "Federação", - "Federation_Description": "A federação permite que um número ilimitado de workspaces se comunique entre si.", "Federation_Enable": "Habilitar federação", "Federation_Example_matrix_server": "Exemplo: matrix.org", "Federation_Federated_room_search": "Busca federada de salas", "Federation_Matrix": "Federação V2", - "Federation_Matrix_Enabled_Alert": "Mais informações sobre o suporte da Matrix Federation podem ser encontradas aqui (Após qualquer configuração, é necessário reiniciar o sistema para que as alterações tenham efeito)", "Federation_Matrix_Federated": "Federado", "Federation_Matrix_Federated_Description": "Ao criar uma sala federada, você não poderá ativar a criptografia nem a transmissão", "Federation_Matrix_Federated_Description_disabled": "A federação está desativada no momento neste workspace", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 87a1808efe1a4..64d891e47eb7c 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -1078,8 +1078,6 @@ "FEDERATION_Domain": "Domínio", "FEDERATION_Domain_Alert": "Não altere isto depois de activar o recurso, ainda não podemos lidar com as alterações de domínio.", "FEDERATION_Domain_Description": "Adicione o domínio a este servidor deve estar ligado por exemplo: @rocket.chat.", - "FEDERATION_Enabled": "Tentativa para a integração do suporte de federação. A alteração deste valor requer a reinicialização do Rocket.Chat.", - "FEDERATION_Enabled_Alert": "O suporte de federação é um trabalho em curso. Não recomendamos o uso num sistema de produção.", "FEDERATION_Public_Key": "Chave Pública", "FEDERATION_Public_Key_Description": "Esta é a chave que pode partilhar com outros.", "FEDERATION_Status": "Estado", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 6cfd7383bcb22..aaaf931fb9125 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -1635,8 +1635,6 @@ "FEDERATION_Domain": "Домен", "FEDERATION_Domain_Alert": "Не изменяйте это после включения функции, мы пока не можем обрабатывать изменения домена.", "FEDERATION_Domain_Description": "Добавьте домен, к которому должен быть привязан этот сервер - например: @ rocket.chat.", - "FEDERATION_Enabled": "Попытка интегрировать поддержку федерации. Изменение этого значения требует перезапуска Rocket.Chat.", - "FEDERATION_Enabled_Alert": "Поддержка федерации находится в стадии разработки. Использование в производственной системе в настоящее время не рекомендуется.", "FEDERATION_Public_Key": "Открытый ключ", "FEDERATION_Public_Key_Description": "Это ключ, которым вы должны поделиться со своими пирами.", "FEDERATION_Status": "Статус", @@ -1667,7 +1665,6 @@ "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Эта функция зависит от выбранного выше поставщика вызовов, который должен быть включен в настройках администрирования.
Для **Jitsi**: убедитесь, что система Jitsi включена в разделе \"Администрирование\" -> \"Видеоконференция\" -> \"Jitsi\" -> \"Включено\".
Для **WebRTC**: убедитесь, что технология WebRTC включена в разделе \"Администрирование\" ->\"WebRTC\" ->\"Включено\".", "Features": "Доступные функции", "Federation": "Федерация", - "Federation_Description": "Федерация позволяет неограниченному числу рабочих пространств взаимодействовать друг с другом.", "Federation_Enable": "Включить федерацию", "Federation_Matrix": "Федерация V2", "Federation_Matrix_enabled": "Включено", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 73f793a5f6665..a8e55794bd6e0 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -2040,8 +2040,6 @@ "FEDERATION_Domain": "Domän", "FEDERATION_Domain_Alert": "Ändra inte det här efter det att du har aktiverat funktionen. Vi kan inte hantera domänändringar ännu.", "FEDERATION_Domain_Description": "Lägg till den domän som servern ska länkas till. Till exempel: @rocket.chat.", - "FEDERATION_Enabled": "Försök att integrera federeringsstöd.", - "FEDERATION_Enabled_Alert": "Federereringsstöd är ett löpande arbete. Användning i ett produktionssystem rekommenderas inte för närvarande.", "FEDERATION_Public_Key": "Öppen nyckel", "FEDERATION_Public_Key_Description": "Det här är den nyckel du delar med dina kollegor.", "FEDERATION_Status": "Status", @@ -2083,12 +2081,10 @@ "Features": "Funktioner", "Federated": "Federerat", "Federation": "Federation", - "Federation_Description": "Med federation kan ett obegränsat antal arbetsytor kommunicera med varandra.", "Federation_Enable": "Aktivera federation", "Federation_Example_matrix_server": "Exempel: matrix.org", "Federation_Federated_room_search": "Federerad rumssökning", "Federation_Matrix": "Federation V2", - "Federation_Matrix_Enabled_Alert": "
Du hittar mer information om stöd för matrisfederering här (Omstart krävs efter konfigurationer för att ändringarna ska träda i kraft)", "Federation_Matrix_Federated": "Federerat", "Federation_Matrix_Federated_Description": "När du skapar ett federerat rum kan du inte aktivera kryptering eller sändning", "Federation_Matrix_Federated_Description_disabled": "Funktionen är inaktiverad i den här arbetsytan.", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 2bcdca4f9ec3f..0d355c0fc2b5e 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -1193,8 +1193,6 @@ "FEDERATION_Domain": "Домен", "FEDERATION_Domain_Alert": "Не змінюйте це після ввімкнення функції, ми ще не можемо обробити зміни домену.", "FEDERATION_Domain_Description": "Додайте домен, до якого повинен бути прив’язаний цей сервер - наприклад: @rocket.chat.", - "FEDERATION_Enabled": "Спроба інтегрувати підтримку федерації.", - "FEDERATION_Enabled_Alert": "Підтримка Федерації - це незавершена робота. Наразі використання у виробничій системі не рекомендується.", "FEDERATION_Public_Key": "Відкритий ключ", "FEDERATION_Public_Key_Description": "Цим ключем потрібно поділитися з Вашими пірами.", "FEDERATION_Status": "Статус", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 27cabe406785e..dd0fbf8a08831 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -1505,8 +1505,6 @@ "FEDERATION_Domain": "網域", "FEDERATION_Domain_Alert": "在啟動功能後不要變更這個,我們無法管理網域變更。", "FEDERATION_Domain_Description": "新增網域然後這個伺服器應該連結到 - example: @rocket.chat。", - "FEDERATION_Enabled": "試著整合聯盟支援", - "FEDERATION_Enabled_Alert": "聯盟支援正在執行中。不建議在這個時候使用系統。", "FEDERATION_Public_Key": "公鑰", "FEDERATION_Public_Key_Description": "這個金鑰需要分享給您的另一個端點", "FEDERATION_Status": "狀態", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index ae8a033e6e58a..3e62415061942 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1357,8 +1357,6 @@ "FEDERATION_Domain": "域名", "FEDERATION_Domain_Alert": "开启此功能后不要更改这里, 当前还不能处理域变更。", "FEDERATION_Domain_Description": "添加此服务器应该关联的域, 如: @rocket.chat。", - "FEDERATION_Enabled": "尝试集成联盟支持。", - "FEDERATION_Enabled_Alert": "联盟支持正在完善中。当前不推荐在生产系统中使用。", "FEDERATION_Public_Key": "公钥", "FEDERATION_Public_Key_Description": "需要分享此凭据给你的对等端", "FEDERATION_Status": "状态", diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 69184ad32e247..1404419554a8c 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -69,8 +69,6 @@ export * from './models/IUsersSessionsModel'; export * from './models/IVideoConferenceModel'; export * from './models/IVoipRoomModel'; export * from './models/IWebdavAccountsModel'; -export * from './models/IMatrixBridgeRoomModel'; -export * from './models/IMatrixBridgeUserModel'; export * from './models/ICalendarEventModel'; export * from './models/IOmnichannelServiceLevelAgreementsModel'; export * from './models/IAppLogsModel'; diff --git a/packages/model-typings/src/models/IMatrixBridgeRoomModel.ts b/packages/model-typings/src/models/IMatrixBridgeRoomModel.ts deleted file mode 100644 index 538e05336a1ad..0000000000000 --- a/packages/model-typings/src/models/IMatrixBridgeRoomModel.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IMatrixBridgedRoom } from '@rocket.chat/core-typings'; - -import type { IBaseModel } from './IBaseModel'; - -export interface IMatrixBridgedRoomModel extends IBaseModel { - getExternalRoomId(localRoomId: string): Promise; - getLocalRoomId(externalRoomId: string): Promise; - removeByLocalRoomId(localRoomId: string): Promise; - createOrUpdateByLocalRoomId(localRoomId: string, externalRoomId: string, fromServer: string): Promise; - getExternalServerConnectedExcluding(exclude: string): Promise; -} diff --git a/packages/model-typings/src/models/IMatrixBridgeUserModel.ts b/packages/model-typings/src/models/IMatrixBridgeUserModel.ts deleted file mode 100644 index 02ad87604fee0..0000000000000 --- a/packages/model-typings/src/models/IMatrixBridgeUserModel.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { IMatrixBridgedUser } from '@rocket.chat/core-typings'; - -import type { IBaseModel } from './IBaseModel'; - -export interface IMatrixBridgedUserModel extends IBaseModel { - getExternalUserIdByLocalUserId(localUserId: string): Promise; - getBridgedUserByExternalUserId(externalUserId: string): Promise; - getLocalUserIdByExternalId(externalUserId: string): Promise; - getLocalUsersByExternalIds(externalUserIds: string[]): Promise; - getBridgedUserByLocalId(localUserId: string): Promise; - createOrUpdateByLocalId(localUserId: string, externalUserId: string, remote: boolean, fromServer: string): Promise; -} diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 6b79ac0c21829..20ba7048ac092 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -103,6 +103,8 @@ export interface IMessagesModel extends IBaseModel { findOneByFederationId(federationEventId: string): Promise; + findLatestFederationThreadMessageByTmid(tmid: string, messageId: IMessage['_id']): Promise; + setFederationEventIdById(_id: string, federationEventId: string): Promise; removeByRoomId(roomId: IRoom['_id']): Promise; diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 86e6e5d45f4c3..f652657b5543f 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -124,7 +124,7 @@ export interface IRoomsModel extends IBaseModel { findByBroadcast(options?: FindOptions): FindCursor; - setAsFederated(roomId: IRoom['_id']): Promise; + setAsFederated(roomId: IRoom['_id'], { mrid, origin }: { mrid: string; origin: string }): Promise; setRoomTypeById(roomId: IRoom['_id'], roomType: IRoom['t']): Promise; @@ -313,4 +313,5 @@ export interface IRoomsModel extends IBaseModel { countByE2E(options?: CountDocumentsOptions): Promise; markRolePrioritesCreatedForRoom(rid: IRoom['_id'], version: number): Promise; hasCreatedRolePrioritiesForRoom(rid: IRoom['_id'], syncVersion: number): Promise; + countDistinctFederationRoomsExcluding(serverNames?: string[]): Promise; } diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 150a40e041df2..121da7e8ad1c3 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -335,4 +335,5 @@ export interface ISubscriptionsModel extends IBaseModel { countByRoomIdWhenUsernameExists(rid: string): Promise; setE2EKeyByUserIdAndRoomId(userId: string, rid: string, key: string): Promise>; countUsersInRoles(roles: IRole['_id'][], rid: IRoom['_id'] | undefined): Promise; + findUserFederatedRoomIds(userId: IUser['_id']): AggregationCursor<{ _id: IRoom['_id']; externalRoomId: string }>; } diff --git a/packages/model-typings/src/models/IUploadsModel.ts b/packages/model-typings/src/models/IUploadsModel.ts index 1e80fcfe39b52..1c0cbb316e427 100644 --- a/packages/model-typings/src/models/IUploadsModel.ts +++ b/packages/model-typings/src/models/IUploadsModel.ts @@ -1,5 +1,5 @@ import type { IRoom, IUpload } from '@rocket.chat/core-typings'; -import type { FindCursor, WithId, Filter, FindOptions } from 'mongodb'; +import type { FindCursor, WithId, Filter, FindOptions, UpdateResult } from 'mongodb'; import type { FindPaginated } from './IBaseModel'; import type { IBaseUploadsModel } from './IBaseUploadsModel'; @@ -14,4 +14,8 @@ export interface IUploadsModel extends IBaseUploadsModel { uploadedAt?: Date, options?: Omit, 'sort'>, ): FindPaginated>>; + + findByFederationMediaIdAndServerName(mediaId: string, serverName: string): Promise; + + setFederationInfo(fileId: IUpload['_id'], info: Required['federation']): Promise; } diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 89c2136d3668a..cdb17caa026e1 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -74,8 +74,6 @@ import type { IVideoConferenceModel, IVoipRoomModel, IWebdavAccountsModel, - IMatrixBridgedRoomModel, - IMatrixBridgedUserModel, ICalendarEventModel, IOmnichannelServiceLevelAgreementsModel, IAppsModel, @@ -217,8 +215,6 @@ export const UsersSessions = proxify('IUsersSessionsModel') export const VideoConference = proxify('IVideoConferenceModel'); export const VoipRoom = proxify('IVoipRoomModel'); export const WebdavAccounts = proxify('IWebdavAccountsModel'); -export const MatrixBridgedRoom = proxify('IMatrixBridgedRoomModel'); -export const MatrixBridgedUser = proxify('IMatrixBridgedUserModel'); export const CalendarEvent = proxify('ICalendarEventModel'); export const OmnichannelServiceLevelAgreements = proxify( 'IOmnichannelServiceLevelAgreementsModel', diff --git a/packages/models/src/modelClasses.ts b/packages/models/src/modelClasses.ts index 9972d15f3372c..12b4b6ea68e0c 100644 --- a/packages/models/src/modelClasses.ts +++ b/packages/models/src/modelClasses.ts @@ -68,8 +68,6 @@ export * from './models/UsersSessions'; export * from './models/VideoConference'; export * from './models/VoipRoom'; export * from './models/WebdavAccounts'; -export * from './models/MatrixBridgedRoom'; -export * from './models/MatrixBridgedUser'; export * from './models/CredentialTokens'; export * from './models/MessageReads'; export * from './models/CronHistoryModel'; diff --git a/packages/models/src/models/MatrixBridgedRoom.ts b/packages/models/src/models/MatrixBridgedRoom.ts deleted file mode 100644 index bca6150a096d4..0000000000000 --- a/packages/models/src/models/MatrixBridgedRoom.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { IMatrixBridgedRoom, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IMatrixBridgedRoomModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, IndexDescription } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; - -export class MatrixBridgedRoomRaw extends BaseRaw implements IMatrixBridgedRoomModel { - constructor(db: Db, trash?: Collection>) { - super(db, 'matrix_bridged_rooms', trash); - } - - protected modelIndexes(): IndexDescription[] { - return [ - { key: { rid: 1 }, unique: true, sparse: true }, - { key: { mri: 1 }, unique: true, sparse: true }, - { key: { fromServer: 1 }, sparse: true }, - ]; - } - - async getExternalRoomId(localRoomId: string): Promise { - const bridgedRoom = await this.findOne({ rid: localRoomId }); - - return bridgedRoom ? bridgedRoom.mri : null; - } - - async getLocalRoomId(externalRoomId: string): Promise { - const bridgedRoom = await this.findOne({ mri: externalRoomId }); - - return bridgedRoom ? bridgedRoom.rid : null; - } - - async removeByLocalRoomId(localRoomId: string): Promise { - await this.deleteOne({ rid: localRoomId }); - } - - async createOrUpdateByLocalRoomId(localRoomId: string, externalRoomId: string, fromServer: string): Promise { - await this.updateOne({ rid: localRoomId }, { $set: { rid: localRoomId, mri: externalRoomId, fromServer } }, { upsert: true }); - } - - async getExternalServerConnectedExcluding(exclude: string): Promise { - const externalServers = await this.col.distinct('fromServer'); - - return externalServers.filter((serverName) => serverName !== exclude); - } -} diff --git a/packages/models/src/models/MatrixBridgedUser.ts b/packages/models/src/models/MatrixBridgedUser.ts deleted file mode 100644 index 37ee374f65cf1..0000000000000 --- a/packages/models/src/models/MatrixBridgedUser.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { IMatrixBridgedUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IMatrixBridgedUserModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, IndexDescription } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; - -export class MatrixBridgedUserRaw extends BaseRaw implements IMatrixBridgedUserModel { - constructor(db: Db, trash?: Collection>) { - super(db, 'matrix_bridged_users', trash); - } - - protected modelIndexes(): IndexDescription[] { - return [ - { key: { uid: 1 }, unique: true, sparse: true }, - { key: { mui: 1 }, unique: true, sparse: true }, - { key: { fromServer: 1 }, sparse: true }, - ]; - } - - async getExternalUserIdByLocalUserId(localUserId: string): Promise { - const bridgedUser = await this.findOne({ uid: localUserId }); - - return bridgedUser ? bridgedUser.mui : null; - } - - async getBridgedUserByExternalUserId(externalUserId: string): Promise { - return this.findOne({ mui: externalUserId }); - } - - async getLocalUserIdByExternalId(externalUserId: string): Promise { - const bridgedUser = await this.findOne({ mui: externalUserId }); - - return bridgedUser ? bridgedUser.uid : null; - } - - async getLocalUsersByExternalIds(externalUserIds: string[]): Promise { - const bridgedUsers = await this.find({ mui: { $in: externalUserIds } }).toArray(); - - return bridgedUsers; - } - - async getBridgedUserByLocalId(localUserId: string): Promise { - return this.findOne({ uid: localUserId }); - } - - async createOrUpdateByLocalId(localUserId: string, externalUserId: string, remote: boolean, fromServer: string): Promise { - await this.updateOne( - { uid: localUserId }, - { - $set: { - uid: localUserId, - mui: externalUserId, - remote, - fromServer, - }, - }, - { upsert: true }, - ); - } -} diff --git a/packages/models/src/models/Messages.ts b/packages/models/src/models/Messages.ts index 50f73cab73d42..452c3fbb05d4d 100644 --- a/packages/models/src/models/Messages.ts +++ b/packages/models/src/models/Messages.ts @@ -604,6 +604,19 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.findOne({ 'federation.eventId': federationEventId }); } + async findLatestFederationThreadMessageByTmid(tmid: string, messageId: IMessage['_id']): Promise { + return this.findOne( + { + '_id': { $ne: messageId }, + tmid, + 'federation.eventId': { $exists: true }, + }, + { + sort: { ts: -1 }, + }, + ); + } + async setFederationEventIdById(_id: string, federationEventId: string): Promise { await this.updateOne( { _id }, diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index d236dd0bcf5b8..43c01e46a057c 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -96,6 +96,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { sparse: true, }, { key: { t: 1, ts: 1 } }, + { + key: { federated: 1 }, + sparse: true, + }, { key: { 'usersWaitingForE2EKeys.userId': 1, @@ -663,8 +667,8 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ); } - setAsFederated(roomId: IRoom['_id']): Promise { - return this.updateOne({ _id: roomId }, { $set: { federated: true } }); + setAsFederated(roomId: IRoom['_id'], { mrid, origin }: { mrid: string; origin: string }): Promise { + return this.updateOne({ _id: roomId }, { $set: { 'federated': true, 'federation.mrid': mrid, 'federation.origin': origin } }); } setRoomTypeById(roomId: IRoom['_id'], roomType: IRoom['t']): Promise { @@ -2209,4 +2213,9 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { async hasCreatedRolePrioritiesForRoom(rid: IRoom['_id'], syncVersion: number) { return this.countDocuments({ _id: rid, rolePrioritiesCreated: syncVersion }); } + + async countDistinctFederationRoomsExcluding(_serverNames: string[] = []): Promise { + // TODO implement + return []; + } } diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index 1829bdcb2d94b..dbd72d644cf11 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -2056,4 +2056,33 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } + + findUserFederatedRoomIds(userId: IUser['_id']): AggregationCursor<{ _id: IRoom['_id']; externalRoomId: string }> { + return this.col.aggregate<{ _id: IRoom['_id']; externalRoomId: string }>([ + { + $match: { + 'u._id': userId, + }, + }, + { + $lookup: { + from: 'rocketchat_room', + localField: 'rid', + foreignField: '_id', + as: 'room', + }, + }, + { + $match: { + 'room.federated': true, + }, + }, + { + $project: { + _id: '$rid', + externalRoomId: { $arrayElemAt: ['$room.federation.mrid', 0] }, + }, + }, + ]); + } } diff --git a/packages/models/src/models/Uploads.ts b/packages/models/src/models/Uploads.ts index fc1b72bce53b1..760080a0cf3b6 100644 --- a/packages/models/src/models/Uploads.ts +++ b/packages/models/src/models/Uploads.ts @@ -2,7 +2,7 @@ import type { IUpload, RocketChatRecordDeleted, IRoom } from '@rocket.chat/core-typings'; import type { FindPaginated, IUploadsModel } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import type { Collection, FindCursor, Db, IndexDescription, WithId, Filter, FindOptions } from 'mongodb'; +import type { Collection, FindCursor, Db, IndexDescription, WithId, Filter, FindOptions, UpdateResult } from 'mongodb'; import { BaseUploadModelRaw } from './BaseUploadModel'; @@ -12,7 +12,12 @@ export class UploadsRaw extends BaseUploadModelRaw implements IUploadsModel { } protected modelIndexes(): IndexDescription[] { - return [...super.modelIndexes(), { key: { uploadedAt: -1 } }, { key: { rid: 1, _hidden: 1, typeGroup: 1 } }]; + return [ + ...super.modelIndexes(), + { key: { uploadedAt: -1 } }, + { key: { rid: 1, _hidden: 1, typeGroup: 1 } }, + { key: { 'federation.mediaId': 1, 'federation.serverName': 1 }, unique: true, sparse: true }, + ]; } findNotHiddenFilesOfRoom(roomId: string, searchText: string, fileType: string, limit: number): FindCursor { @@ -47,6 +52,14 @@ export class UploadsRaw extends BaseUploadModelRaw implements IUploadsModel { }); } + findByFederationMediaIdAndServerName(mediaId: string, serverName: string): Promise { + return this.findOne({ 'federation.mediaId': mediaId, 'federation.serverName': serverName }); + } + + setFederationInfo(fileId: IUpload['_id'], info: Required['federation']): Promise { + return this.updateOne({ _id: fileId }, { $set: { federation: info } }); + } + findPaginatedWithoutThumbs(query: Filter = {}, options?: FindOptions): FindPaginated>> { return this.findPaginated( { diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 0481483ccfb54..f84411ff50953 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -1548,6 +1548,9 @@ export class UsersRaw extends BaseRaw> implements IU const update = { $set: { federated: true, + federation: { + version: 1, + }, }, }; return this.updateOne(query, update); diff --git a/yarn.lock b/yarn.lock index 6e9e582970af7..24daf7f32c5c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -118,7 +118,34 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1": +"@babel/cli@npm:~7.26.0": + version: 7.26.4 + resolution: "@babel/cli@npm:7.26.4" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.25" + "@nicolo-ribaudo/chokidar-2": "npm:2.1.8-no-fsevents.3" + chokidar: "npm:^3.6.0" + commander: "npm:^6.2.0" + convert-source-map: "npm:^2.0.0" + fs-readdir-recursive: "npm:^1.1.0" + glob: "npm:^7.2.0" + make-dir: "npm:^2.1.0" + slash: "npm:^2.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + dependenciesMeta: + "@nicolo-ribaudo/chokidar-2": + optional: true + chokidar: + optional: true + bin: + babel: ./bin/babel.js + babel-external-helpers: ./bin/babel-external-helpers.js + checksum: 10/4123d8a3cb9fa3a54595242dd49dfc3da3575837fcf5e9072addd8d0d55eeab52b2e37e6d10ecd9f131d7a29e3265ed8f288de84ba1955767b3fd6968f9cbd00 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.25.7, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" dependencies: @@ -159,7 +186,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:~7.26.10": +"@babel/core@npm:~7.26.0, @babel/core@npm:~7.26.10": version: 7.26.10 resolution: "@babel/core@npm:7.26.10" dependencies: @@ -209,6 +236,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.25.7, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.27.1": + version: 7.28.3 + resolution: "@babel/generator@npm:7.28.3" + dependencies: + "@babel/parser": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10/d00d1e6b51059e47594aab7920b88ec6fcef6489954a9172235ab57ad2e91b39c95376963a6e2e4cc7e8b88fa4f931018f71f9ab32bbc9c0bc0de35a0231f26c + languageName: node + linkType: hard + "@babel/generator@npm:^7.28.0": version: 7.28.0 resolution: "@babel/generator@npm:7.28.0" @@ -381,14 +421,14 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.27.1": +"@babel/helper-string-parser@npm:^7.23.4, @babel/helper-string-parser@npm:^7.25.7, @babel/helper-string-parser@npm:^7.25.9, @babel/helper-string-parser@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-string-parser@npm:7.27.1" checksum: 10/0ae29cc2005084abdae2966afdb86ed14d41c9c37db02c3693d5022fba9f5d59b011d039380b8e537c34daf117c549f52b452398f576e908fb9db3c7abbb3a00 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": +"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.25.7, @babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-identifier@npm:7.27.1" checksum: 10/75041904d21bdc0cd3b07a8ac90b11d64cd3c881e89cb936fa80edd734bf23c35e6bd1312611e8574c4eab1f3af0f63e8a5894f4699e9cfdf70c06fcf4252320 @@ -445,6 +485,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.27.1, @babel/parser@npm:^7.28.3": + version: 7.28.4 + resolution: "@babel/parser@npm:7.28.4" + dependencies: + "@babel/types": "npm:^7.28.4" + bin: + parser: ./bin/babel-parser.js + checksum: 10/f54c46213ef180b149f6a17ea765bf40acc1aebe2009f594e2a283aec69a190c6dda1fdf24c61a258dbeb903abb8ffb7a28f1a378f8ab5d333846ce7b7e23bf1 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" @@ -1412,7 +1463,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:~7.26.9": +"@babel/preset-env@npm:~7.26.0, @babel/preset-env@npm:~7.26.9": version: 7.26.9 resolution: "@babel/preset-env@npm:7.26.9" dependencies: @@ -1550,7 +1601,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2, @babel/runtime@npm:~7.26.10": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2, @babel/runtime@npm:~7.26.10": version: 7.26.10 resolution: "@babel/runtime@npm:7.26.10" dependencies: @@ -1559,7 +1610,16 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": +"@babel/runtime@npm:^7.25.6": + version: 7.26.7 + resolution: "@babel/runtime@npm:7.26.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/c7a661a6836b332d9d2e047cba77ba1862c1e4f78cec7146db45808182ef7636d8a7170be9797e5d8fd513180bffb9fa16f6ca1c69341891efec56113cf22bfc + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.26.9, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" dependencies: @@ -1570,18 +1630,55 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/traverse@npm:7.27.4" +"@babel/template@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/template@npm:7.25.7" dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.3" - "@babel/parser": "npm:^7.27.4" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.3" + "@babel/code-frame": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10/49e1e88d2eac17d31ae28d6cf13d6d29c1f49384c4f056a6751c065d6565c351e62c01ce6b11fef5edb5f3a77c87e114ea7326ca384fa618b4834e10cf9b20f3 + languageName: node + linkType: hard + +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/generator": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/4debb80b9068a46e188e478272f3b6820e16d17e2651e82d0a0457176b0c3b2489994f0a0d6e8941ee90218b0a8a69fe52ba350c1aa66eb4c72570d6b2405f91 + checksum: 10/7431614d76d4a053e429208db82f2846a415833f3d9eb2e11ef72eeb3c64dfd71f4a4d983de1a4a047b36165a1f5a64de8ca2a417534cc472005c740ffcb9c6a + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.18.9": + version: 7.25.7 + resolution: "@babel/traverse@npm:7.25.7" + dependencies: + "@babel/code-frame": "npm:^7.25.7" + "@babel/generator": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/template": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/5b2d332fcd6bc78e6500c997e79f7e2a54dfb357e06f0908cb7f0cdd9bb54e7fd3c5673f45993849d433d01ea6076a6d04b825958f0cfa01288ad55ffa5c286f languageName: node linkType: hard @@ -1600,7 +1697,69 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": + version: 7.27.4 + resolution: "@babel/traverse@npm:7.27.4" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.27.3" + "@babel/parser": "npm:^7.27.4" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.27.3" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/4debb80b9068a46e188e478272f3b6820e16d17e2651e82d0a0457176b0c3b2489994f0a0d6e8941ee90218b0a8a69fe52ba350c1aa66eb4c72570d6b2405f91 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/traverse@npm:7.27.1" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.27.1" + "@babel/parser": "npm:^7.27.1" + "@babel/template": "npm:^7.27.1" + "@babel/types": "npm:^7.27.1" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/9977271aa451293d3f184521412788d6ddaff9d6a29626d7435b5dacd059feb2d7753bc94f59f4f5b76e65bd2e2cabc8a10d7e1f93709feda28619f2e8cbf4d6 + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.23.5 + resolution: "@babel/types@npm:7.23.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.23.4" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/a623a4e7f396f1903659099da25bfa059694a49f42820f6b5288347f1646f0b37fb7cc550ba45644e9067149368ef34ccb1bd4a4251ec59b83b3f7765088f363 + languageName: node + linkType: hard + +"@babel/types@npm:^7.18.9, @babel/types@npm:^7.25.7": + version: 7.25.8 + resolution: "@babel/types@npm:7.25.8" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/973108dbb189916bb87360f2beff43ae97f1b08f1c071bc6499d363cce48b3c71674bf3b59dfd617f8c5062d1c76dc2a64232bc07b6ccef831fd0c06162d44d9 + languageName: node + linkType: hard + +"@babel/types@npm:^7.25.9": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + +"@babel/types@npm:^7.26.10, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6": version: 7.27.6 resolution: "@babel/types@npm:7.27.6" dependencies: @@ -1610,6 +1769,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/types@npm:7.27.1" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10/81f8ada28c4b29695d7d4c4cbfaa5ec3138ccebbeb26628c7c3cc570fdc84f28967c9e68caf4977d51ff4f4d3159c88857ef278317f84f3515dd65e5b8a74995 + languageName: node + linkType: hard + "@babel/types@npm:^7.28.0": version: 7.28.0 resolution: "@babel/types@npm:7.28.0" @@ -1620,6 +1789,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/types@npm:7.28.4" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -2169,6 +2348,22 @@ __metadata: languageName: node linkType: hard +"@datastructures-js/heap@npm:^4.3.3": + version: 4.3.5 + resolution: "@datastructures-js/heap@npm:4.3.5" + checksum: 10/9360ae87e517aaf547251db4faea77388511dd7e4e554da1a3857e072cf34edba4e33faf8c93d738c3a78ddcdee903faefd02238371f0b201bb129d73f03a2ef + languageName: node + linkType: hard + +"@datastructures-js/priority-queue@npm:^6.3.3": + version: 6.3.4 + resolution: "@datastructures-js/priority-queue@npm:6.3.4" + dependencies: + "@datastructures-js/heap": "npm:^4.3.3" + checksum: 10/7c2fbfc1c3a1f9d1f1d0c540a38f41865400d72dc40c13d621657386513ed5faddba4366dcf76b4e52accefa1be67d13602f73bc4a43afa96603071f78b427fe + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -2623,6 +2818,15 @@ __metadata: languageName: node linkType: hard +"@hono/node-server@npm:^1.14.4": + version: 1.14.4 + resolution: "@hono/node-server@npm:1.14.4" + peerDependencies: + hono: ^4 + checksum: 10/3cbe4133507ae6da949f5f34b74a0d84aaef597710b14675c773f4349a65b1bcdafc2503df26c409104626d23a18ca0c2783fe790d509478b117a85f1984f518 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -3731,6 +3935,16 @@ __metadata: languageName: node linkType: hard +"@matrix-org/matrix-sdk-crypto-nodejs@npm:0.4.0-beta.1": + version: 0.4.0-beta.1 + resolution: "@matrix-org/matrix-sdk-crypto-nodejs@npm:0.4.0-beta.1" + dependencies: + https-proxy-agent: "npm:^7.0.5" + node-downloader-helper: "npm:^2.1.9" + checksum: 10/a1402d18b166cd9fc8122ae40c40f179f1df225dd7c98b8c89ef7a00f94a08256e988ab923d79c2aa44c6dd050792ee4f787ecdbde3c88b276fba96558ae0f50 + languageName: node + linkType: hard + "@mdx-js/react@npm:^3.0.0": version: 3.0.1 resolution: "@mdx-js/react@npm:3.0.1" @@ -3937,6 +4151,13 @@ __metadata: languageName: node linkType: hard +"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": + version: 2.1.8-no-fsevents.3 + resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" + checksum: 10/c6e83af3b5051a3f6562649ff8fe37de9934a4cc02138678ed1badbd13ed3334f7ae5f63f2bbc3432210f6b245f082ac97e9b2afe0c13730c9838b295658c185 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -4171,6 +4392,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^3.0.0": + version: 3.0.0 + resolution: "@noble/ed25519@npm:3.0.0" + checksum: 10/b188ed76309aa172633f853056d6647b6e5491e9c60f2db4e5a9d4398c3dc3529f4d02fbf88530dc4e369d7ef23ec0015006a6798fbe1ca339732d0a3a0de7f1 + languageName: node + linkType: hard + "@noble/hashes@npm:^1.1.5": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" @@ -7105,6 +7333,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/http-router": "workspace:^" "@rocket.chat/icons": "npm:^0.43.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/media-signaling": "workspace:^" @@ -7124,7 +7353,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/core-typings@workspace:^, @rocket.chat/core-typings@workspace:packages/core-typings, @rocket.chat/core-typings@workspace:~": +"@rocket.chat/core-typings@workspace:*, @rocket.chat/core-typings@workspace:^, @rocket.chat/core-typings@workspace:packages/core-typings, @rocket.chat/core-typings@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: @@ -7263,7 +7492,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/emitter@npm:~0.31.25": +"@rocket.chat/emitter@npm:^0.31.25, @rocket.chat/emitter@npm:~0.31.25": version: 0.31.25 resolution: "@rocket.chat/emitter@npm:0.31.25" checksum: 10/fee26d0200d60eadb246e4e2b40f99bbfaa6f748d11cb8fbbe350219a178630950b1ecbd6145a5dc93f8ff0298afdaef665f544f82bde7b3d0c687a298b9a1e3 @@ -7302,6 +7531,93 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/federation-matrix@workspace:^, @rocket.chat/federation-matrix@workspace:ee/packages/federation-matrix": + version: 0.0.0-use.local + resolution: "@rocket.chat/federation-matrix@workspace:ee/packages/federation-matrix" + dependencies: + "@babel/cli": "npm:~7.26.0" + "@babel/core": "npm:~7.26.0" + "@babel/preset-env": "npm:~7.26.0" + "@babel/preset-typescript": "npm:~7.26.0" + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": "npm:^0.31.25" + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/federation-sdk": "npm:0.1.8" + "@rocket.chat/http-router": "workspace:^" + "@rocket.chat/license": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@types/emojione": "npm:^2.2.9" + "@types/node": "npm:~22.14.0" + "@types/sanitize-html": "npm:^2" + "@vector-im/matrix-bot-sdk": "npm:^0.7.1-element.6" + babel-jest: "npm:~30.0.0" + emojione: "npm:^4.5.0" + eslint: "npm:~8.45.0" + jest: "npm:~30.0.0" + marked: "npm:^16.1.2" + mongodb: "npm:6.10.0" + pino: "npm:^9.11.0" + pino-pretty: "npm:^7.6.1" + reflect-metadata: "npm:^0.2.2" + sanitize-html: "npm:^2.17.0" + tsyringe: "npm:^4.10.0" + tweetnacl: "npm:^1.0.3" + typescript: "npm:~5.8.3" + languageName: unknown + linkType: soft + +"@rocket.chat/federation-sdk@npm:0.1.8": + version: 0.1.8 + resolution: "@rocket.chat/federation-sdk@npm:0.1.8" + dependencies: + "@datastructures-js/priority-queue": "npm:^6.3.3" + "@noble/ed25519": "npm:^3.0.0" + "@rocket.chat/emitter": "npm:^0.31.25" + mongodb: "npm:^6.16.0" + pino: "npm:^9.11.0" + reflect-metadata: "npm:^0.2.2" + tsyringe: "npm:^4.10.0" + tweetnacl: "npm:^1.0.3" + zod: "npm:^3.22.4" + peerDependencies: + typescript: ~5.9.2 + checksum: 10/8a3d05a82ced70d462527514a9ea8ba8469635f085b88ef568a6ccabfcd77a4dd979d610b56fe263fc3915a073d49585ac52863fd14a98aae5a89ebdc605bf5b + languageName: node + linkType: hard + +"@rocket.chat/federation-service@workspace:^, @rocket.chat/federation-service@workspace:ee/apps/federation-service": + version: 0.0.0-use.local + resolution: "@rocket.chat/federation-service@workspace:ee/apps/federation-service" + dependencies: + "@hono/node-server": "npm:^1.14.4" + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:*" + "@rocket.chat/emitter": "npm:^0.31.25" + "@rocket.chat/federation-matrix": "workspace:^" + "@rocket.chat/federation-sdk": "npm:0.1.8" + "@rocket.chat/http-router": "workspace:*" + "@rocket.chat/instance-status": "workspace:^" + "@rocket.chat/license": "workspace:^" + "@rocket.chat/models": "workspace:*" + "@rocket.chat/network-broker": "workspace:^" + "@types/bun": "npm:latest" + "@types/express": "npm:^4.17.17" + eslint: "npm:~8.45.0" + hono: "npm:^3.11.0" + pino: "npm:^9.11.0" + pino-pretty: "npm:^7.6.1" + polka: "npm:^0.5.2" + reflect-metadata: "npm:^0.2.2" + tsyringe: "npm:^4.10.0" + tweetnacl: "npm:^1.0.3" + typescript: "npm:^5.3.0" + zod: "npm:^3.22.4" + languageName: unknown + linkType: soft + "@rocket.chat/freeswitch@workspace:^, @rocket.chat/freeswitch@workspace:packages/freeswitch": version: 0.0.0-use.local resolution: "@rocket.chat/freeswitch@workspace:packages/freeswitch" @@ -7553,7 +7869,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/http-router@workspace:^, @rocket.chat/http-router@workspace:packages/http-router": +"@rocket.chat/http-router@workspace:*, @rocket.chat/http-router@workspace:^, @rocket.chat/http-router@workspace:packages/http-router": version: 0.0.0-use.local resolution: "@rocket.chat/http-router@workspace:packages/http-router" dependencies: @@ -7924,6 +8240,7 @@ __metadata: "@babel/runtime": "npm:~7.26.10" "@bugsnag/js": "npm:~7.20.2" "@bugsnag/plugin-react": "npm:~7.19.0" + "@datastructures-js/priority-queue": "npm:^6.3.3" "@faker-js/faker": "npm:~8.0.2" "@google-cloud/storage": "npm:^7.15.0" "@kaciras/deasync": "npm:^1.1.0" @@ -7932,6 +8249,7 @@ __metadata: "@nivo/heatmap": "npm:0.88.0" "@nivo/line": "npm:0.88.0" "@nivo/pie": "npm:0.88.0" + "@noble/ed25519": "npm:^3.0.0" "@node-oauth/oauth2-server": "npm:5.2.0" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.54.2" @@ -7955,6 +8273,8 @@ __metadata: "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" + "@rocket.chat/federation-matrix": "workspace:^" + "@rocket.chat/federation-service": "workspace:^" "@rocket.chat/freeswitch": "workspace:^" "@rocket.chat/fuselage": "npm:^0.66.4" "@rocket.chat/fuselage-forms": "npm:^0.1.0" @@ -8269,6 +8589,7 @@ __metadata: react-keyed-flatten-children: "npm:^3.0.2" react-stately: "npm:~3.17.0" react-virtuoso: "npm:^4.12.0" + reflect-metadata: "npm:^0.2.2" sanitize-html: "npm:^2.14.0" semver: "npm:^7.6.3" sharp: "npm:^0.33.5" @@ -8294,6 +8615,8 @@ __metadata: textarea-caret: "npm:^3.1.0" tinykeys: "npm:^1.4.0" ts-node: "npm:^10.9.2" + tsyringe: "npm:^4.10.0" + tweetnacl: "npm:^1.0.3" twilio: "npm:^5.4.2" twit: "npm:^2.2.11" typescript: "npm:~5.9.2" @@ -8357,7 +8680,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/models@workspace:^, @rocket.chat/models@workspace:packages/models": +"@rocket.chat/models@workspace:*, @rocket.chat/models@workspace:^, @rocket.chat/models@workspace:packages/models": version: 0.0.0-use.local resolution: "@rocket.chat/models@workspace:packages/models" dependencies: @@ -10967,6 +11290,15 @@ __metadata: languageName: node linkType: hard +"@types/bun@npm:latest": + version: 1.2.16 + resolution: "@types/bun@npm:1.2.16" + dependencies: + bun-types: "npm:1.2.16" + checksum: 10/aaa67912ed6fe57a77682cb023996d7d6fb33487a0efd52fa770cc2be4a1a84924d67a45b8788d7741d85c6d92b1a33c11665daba9c8955e5abe7cdda1f1980e + languageName: node + linkType: hard + "@types/busboy@npm:^1.5.4": version: 1.5.4 resolution: "@types/busboy@npm:1.5.4" @@ -11515,7 +11847,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.21, @types/express@npm:^4.17.23": +"@types/express@npm:*, @types/express@npm:^4.17.17, @types/express@npm:^4.17.23": version: 4.17.23 resolution: "@types/express@npm:4.17.23" dependencies: @@ -11527,6 +11859,18 @@ __metadata: languageName: node linkType: hard +"@types/express@npm:^4.16.1, @types/express@npm:^4.17.21": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/7a6d26cf6f43d3151caf4fec66ea11c9d23166e4f3102edfe45a94170654a54ea08cf3103d26b3928d7ebcc24162c90488e33986b7e3a5f8941225edd5eb18c7 + languageName: node + linkType: hard + "@types/fibers@npm:^3.1.4": version: 3.1.4 resolution: "@types/fibers@npm:3.1.4" @@ -12044,6 +12388,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:~22.14.0": + version: 22.14.1 + resolution: "@types/node@npm:22.14.1" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10/561b1ad98ef5176d6da856ffbbe494f16655149f6a7d561de0423c8784910c81267d7d6459f59d68a97b3cbae9b5996b3b5dfe64f4de3de2239d295dcf4a4dcc + languageName: node + linkType: hard + "@types/nodemailer@npm:*, @types/nodemailer@npm:^6.4.17": version: 6.4.17 resolution: "@types/nodemailer@npm:6.4.17" @@ -12260,6 +12613,15 @@ __metadata: languageName: node linkType: hard +"@types/sanitize-html@npm:^2": + version: 2.16.0 + resolution: "@types/sanitize-html@npm:2.16.0" + dependencies: + htmlparser2: "npm:^8.0.0" + checksum: 10/988cbdecce06b858fc5c92ed5573eb984852234be4ea4001ad703a9f0a00a491d788cfb0e3002b2cc01180e2598e7c8f9e5836fbe795601740aa91df3345d564 + languageName: node + linkType: hard + "@types/sanitize-html@npm:^2.13.0": version: 2.13.0 resolution: "@types/sanitize-html@npm:2.13.0" @@ -13107,6 +13469,32 @@ __metadata: languageName: node linkType: hard +"@vector-im/matrix-bot-sdk@npm:^0.7.1-element.6": + version: 0.7.1-element.14 + resolution: "@vector-im/matrix-bot-sdk@npm:0.7.1-element.14" + dependencies: + "@matrix-org/matrix-sdk-crypto-nodejs": "npm:0.4.0-beta.1" + "@types/express": "npm:^4.17.21" + another-json: "npm:^0.2.0" + async-lock: "npm:^1.4.0" + chalk: "npm:4" + express: "npm:^4.21.2" + glob-to-regexp: "npm:^0.4.1" + hash.js: "npm:^1.1.7" + html-to-text: "npm:^9.0.5" + htmlencode: "npm:^0.0.4" + lowdb: "npm:1" + lru-cache: "npm:^10.0.1" + mkdirp: "npm:^3.0.1" + morgan: "npm:^1.10.0" + postgres: "npm:^3.4.1" + request: "npm:^2.88.2" + request-promise: "npm:^4.2.6" + sanitize-html: "npm:^2.11.0" + checksum: 10/2f995663ceed1cfed1d4fd3d8828293f98733915943edc2e74d4ca64ee6e92e5362e56c00a41c236c0947448e0b1e398352c8c6eb0bbcc9569dbda0a39b12c76 + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:~4.5.2": version: 4.5.2 resolution: "@vitejs/plugin-react@npm:4.5.2" @@ -14448,7 +14836,7 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:30.0.5, babel-jest@npm:~30.0.5": +"babel-jest@npm:30.0.5, babel-jest@npm:~30.0.0, babel-jest@npm:~30.0.5": version: 30.0.5 resolution: "babel-jest@npm:30.0.5" dependencies: @@ -15467,6 +15855,15 @@ __metadata: languageName: node linkType: hard +"bun-types@npm:1.2.16": + version: 1.2.16 + resolution: "bun-types@npm:1.2.16" + dependencies: + "@types/node": "npm:*" + checksum: 10/c64962b32fc0d43f67cca4dda7632bfe9f3ca784a9d0217236fb249d84a1185e6f164165ad3d177c1b6dc64b14890594da1997fc1a9adc855d5057f9ad3fb2a0 + languageName: node + linkType: hard + "bundle-name@npm:^4.1.0": version: 4.1.0 resolution: "bundle-name@npm:4.1.0" @@ -16471,7 +16868,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.1.0": +"commander@npm:^6.1.0, commander@npm:^6.2.0": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e @@ -20920,6 +21317,13 @@ __metadata: languageName: node linkType: hard +"fs-readdir-recursive@npm:^1.1.0": + version: 1.1.0 + resolution: "fs-readdir-recursive@npm:1.1.0" + checksum: 10/d5e3fd8456b8e5d57a43f169a9eaf65c70fa82c4a22f1d4361cdba4ea5e61c60c5c2b4ac481ea137a4d43b2b99b3ea2fae95ac2730255c4206d61af645866c3a + languageName: node + linkType: hard + "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -21885,6 +22289,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^3.11.0": + version: 3.12.12 + resolution: "hono@npm:3.12.12" + checksum: 10/1020c90065e0824b4dc24e3326da081a634ce4b95b04c6d3b695d7dfb3bcc9cc66ace506fff5fdd2ec560891809eb974cede2e634ba79405726e8f690f2c3efc + languageName: node + linkType: hard + "hono@npm:^4.6.19": version: 4.6.19 resolution: "hono@npm:4.6.19" @@ -24830,7 +25241,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:~30.0.2, jest@npm:~30.0.5": +"jest@npm:~30.0.0, jest@npm:~30.0.2, jest@npm:~30.0.5": version: 30.0.5 resolution: "jest@npm:30.0.5" dependencies: @@ -26173,6 +26584,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^16.1.2": + version: 16.1.2 + resolution: "marked@npm:16.1.2" + bin: + marked: bin/marked.js + checksum: 10/190d9b206f05d87a7acac3b50ab19505878297971a0c5652a9d4fa2b022407f22d2b79e1aa1e9f23a32c0158b1f5852ad33da2e83cc12100116a8fc0afc2b17e + languageName: node + linkType: hard + "marked@npm:^4.3.0": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -27282,7 +27702,7 @@ __metadata: version: 3.3.8 resolution: "nanoid@npm:3.3.8" bin: - nanoid: bin/nanoid.cjs + nanoid: bin/nanoid.js checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 languageName: node linkType: hard @@ -28050,14 +28470,14 @@ __metadata: linkType: hard "open@npm:^10.0.3": - version: 10.1.0 - resolution: "open@npm:10.1.0" + version: 10.2.0 + resolution: "open@npm:10.2.0" dependencies: default-browser: "npm:^5.2.1" define-lazy-prop: "npm:^3.0.0" is-inside-container: "npm:^1.0.0" - is-wsl: "npm:^3.1.0" - checksum: 10/a9c4105243a1b3c5312bf2aeb678f78d31f00618b5100088ee01eed2769963ea1f2dd464ac8d93cef51bba2d911e1a9c0c34a753ec7b91d6b22795903ea6647a + wsl-utils: "npm:^0.1.0" + checksum: 10/e6ad9474734eac3549dcc7d85e952394856ccaee48107c453bd6a725b82e3b8ed5f427658935df27efa76b411aeef62888edea8a9e347e8e7c82632ec966b30e languageName: node linkType: hard @@ -29022,6 +29442,15 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/e5699ecb06c7121055978e988e5cecea5b6892fc2589c64f1f86df5e7386bbbfd2ada268839e911b021c6b3123428aed7c6be3ac7940eee139556c75324c7e83 + languageName: node + linkType: hard + "pino-pretty@npm:^7.6.1": version: 7.6.1 resolution: "pino-pretty@npm:7.6.1" @@ -29052,6 +29481,13 @@ __metadata: languageName: node linkType: hard +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10/884e08f65aa5463d820521ead3779d4472c78fc434d8582afb66f9dcb8d8c7119c69524b68106cb8caf92c0487be7794cf50e5b9c0383ae65b24bf2a03480951 + languageName: node + linkType: hard + "pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" @@ -29073,6 +29509,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:^9.11.0": + version: 9.11.0 + resolution: "pino@npm:9.11.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10/359bc3624110a0261a5dc5fc3f990028920a8165d173bd5304b328da3ed9eb1281d233c2acfb1a263282fed0aa1a1e1d5f2f66e856fcb56926836458610e78bc + languageName: node + linkType: hard + "pirates@npm:^4.0.4, pirates@npm:^4.0.6, pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -30121,6 +30578,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^5.0.0": + version: 5.0.0 + resolution: "process-warning@npm:5.0.0" + checksum: 10/10f3e00ac9fc1943ec4566ff41fff2b964e660f853c283e622257719839d340b4616e707d62a02d6aa0038761bb1fa7c56bc7308d602d51bd96f05f9cd305dcd + languageName: node + linkType: hard + "process@npm:^0.10.0": version: 0.10.1 resolution: "process@npm:0.10.1" @@ -31321,6 +31785,13 @@ __metadata: languageName: node linkType: hard +"reflect-metadata@npm:^0.2.2": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: 10/1c93f9ac790fea1c852fde80c91b2760420069f4862f28e6fae0c00c6937a56508716b0ed2419ab02869dd488d123c4ab92d062ae84e8739ea7417fae10c4745 + languageName: node + linkType: hard + "reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" @@ -32147,6 +32618,20 @@ __metadata: languageName: node linkType: hard +"sanitize-html@npm:^2.17.0": + version: 2.17.0 + resolution: "sanitize-html@npm:2.17.0" + dependencies: + deepmerge: "npm:^4.2.2" + escape-string-regexp: "npm:^4.0.0" + htmlparser2: "npm:^8.0.0" + is-plain-object: "npm:^5.0.0" + parse-srcset: "npm:^1.0.2" + postcss: "npm:^8.3.11" + checksum: 10/93a91c629b91f1ad25ede5cd000d4212f3ed495a9b8eeb2cb1b50c936807ab11e736d6c6a75d141daac28430d14e40351981809fbb05f7be7bdffb60318cfebd + languageName: node + linkType: hard + "sass-loader@npm:~16.0.5": version: 16.0.5 resolution: "sass-loader@npm:16.0.5" @@ -32833,6 +33318,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^2.0.0": + version: 2.0.0 + resolution: "slash@npm:2.0.0" + checksum: 10/512d4350735375bd11647233cb0e2f93beca6f53441015eea241fe784d8068281c3987fbaa93e7ef1c38df68d9c60013045c92837423c69115297d6169aa85e6 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -32963,6 +33455,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/385ef7fb5ea5976c1d2a1fef0b6df8df6b7caba8696d2d67f689d60c05e3ea2d536752ce7e1c69b9fad844635f1036d07c446f8e8149f5c6a80e0040a455b310 + languageName: node + linkType: hard + "sort-keys-length@npm:^1.0.0": version: 1.0.1 resolution: "sort-keys-length@npm:1.0.1" @@ -34476,6 +34977,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10/ea2d816c4f6077a7062fac5414a88e82977f807c82ee330938fb9691fe11883bb03f078551c0518bb649c239e47ba113d44014fcbb5db42c5abd5996f35e4213 + languageName: node + linkType: hard + "thriftrw@npm:^3.5.0": version: 3.12.0 resolution: "thriftrw@npm:3.12.0" @@ -34642,6 +35152,13 @@ __metadata: languageName: node linkType: hard +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: 10/be2de62fe58ead94e3e592680052683b1ec986c72d589e7b21e5697f8744cdbf48c266fa72f6c15932894c10187b5f54573a3bcf7da0bfd964d5caf23d436168 + languageName: node + linkType: hard + "to-no-case@npm:^1.0.0": version: 1.0.2 resolution: "to-no-case@npm:1.0.2" @@ -34978,7 +35495,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1": +"tslib@npm:^1.8.1, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb @@ -35010,6 +35527,15 @@ __metadata: languageName: node linkType: hard +"tsyringe@npm:^4.10.0": + version: 4.10.0 + resolution: "tsyringe@npm:4.10.0" + dependencies: + tslib: "npm:^1.9.3" + checksum: 10/b42660dc112cee2db02b3d69f2ef6a6a9d185afd96b18d8f88e47c1e62be94b69a9f5a58fcfdb2a3fbb7c6c175b8162ea00f7db6499bf333ce945e570e31615c + languageName: node + linkType: hard + "ttl@npm:^1.3.0": version: 1.3.1 resolution: "ttl@npm:1.3.1" @@ -35111,7 +35637,7 @@ __metadata: languageName: node linkType: hard -"tweetnacl@npm:1.0.3": +"tweetnacl@npm:1.0.3, tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3" checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 @@ -35319,7 +35845,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.9.2": +"typescript@npm:^5.3.0, typescript@npm:~5.9.2": version: 5.9.2 resolution: "typescript@npm:5.9.2" bin: @@ -35329,7 +35855,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A~5.9.2#optional!builtin": +"typescript@npm:~5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.3.0#optional!builtin, typescript@patch:typescript@npm%3A~5.9.2#optional!builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" bin: @@ -35339,6 +35875,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/b9b1e73dabac5dc730c041325dbd9c99467c1b0d239f1b74ec3b90d831384af3e2ba973946232df670519147eb51a2c20f6f96163cea2b359f03de1e2091cc4f + languageName: node + linkType: hard + "typia@npm:~9.7.0": version: 9.7.0 resolution: "typia@npm:9.7.0" @@ -36820,6 +37366,15 @@ __metadata: languageName: node linkType: hard +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10/de4c92187e04c3c27b4478f410a02e81c351dc85efa3447bf1666f34fc80baacd890a6698ec91995631714086992036013286aea3d77e6974020d40a08e00aec + languageName: node + linkType: hard + "xml-crypto@npm:~3.2.1": version: 3.2.1 resolution: "xml-crypto@npm:3.2.1" @@ -37138,6 +37693,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.22.4": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard + "zod@npm:^3.24.1": version: 3.24.1 resolution: "zod@npm:3.24.1"