From 91d4299bf85257f009e8017bdcea55532638c13f Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 30 Jun 2025 22:02:45 -0300 Subject: [PATCH 01/99] creates federation-service app --- ee/apps/federation-service/.eslintrc.json | 4 ++ ee/apps/federation-service/package.json | 47 +++++++++++++++ ee/apps/federation-service/src/config.ts | 23 +++++++ ee/apps/federation-service/src/service.ts | 73 +++++++++++++++++++++++ ee/apps/federation-service/tsconfig.json | 19 ++++++ 5 files changed, 166 insertions(+) create mode 100644 ee/apps/federation-service/.eslintrc.json create mode 100644 ee/apps/federation-service/package.json create mode 100644 ee/apps/federation-service/src/config.ts create mode 100644 ee/apps/federation-service/src/service.ts create mode 100644 ee/apps/federation-service/tsconfig.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..de94f694d90fb --- /dev/null +++ b/ee/apps/federation-service/package.json @@ -0,0 +1,47 @@ +{ + "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/homeserver": "workspace:*", + "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/models": "workspace:*", + "hono": "^3.11.0", + "pino": "^8.16.0", + "polka": "^0.5.2", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^4.17.17", + "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..0469553f8ab56 --- /dev/null +++ b/ee/apps/federation-service/src/config.ts @@ -0,0 +1,23 @@ +export type Config = { + port: number; + host: string; + routePrefix: string; + rocketchatUrl: string; + authMode: 'jwt' | 'api-key' | 'internal'; + logLevel: 'debug' | 'info' | 'warn' | 'error'; + nodeEnv: 'development' | 'production' | 'test'; +}; + +export function isRunningMs(): boolean { + return !!process.env.TRANSPORTER?.match(/^(?:nats|TCP)/); +} + +export const config = { + port: parseInt(process.env.FEDERATION_SERVICE_PORT || '3030'), + host: process.env.FEDERATION_SERVICE_HOST || '0.0.0.0', + routePrefix: process.env.FEDERATION_ROUTE_PREFIX || '/_matrix', + rocketchatUrl: process.env.ROCKETCHAT_URL || '', + authMode: (process.env.FEDERATION_AUTH_MODE as any) || 'jwt', + logLevel: (process.env.LOG_LEVEL as any) || 'info', + nodeEnv: (process.env.NODE_ENV as any) || 'development', +}; diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts new file mode 100644 index 0000000000000..a5016d431e812 --- /dev/null +++ b/ee/apps/federation-service/src/service.ts @@ -0,0 +1,73 @@ +import 'reflect-metadata'; +import { serve } from '@hono/node-server'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; +import type { RouteDefinition, RouteContext } from '@rocket.chat/homeserver'; +import { registerServiceModels } from '@rocket.chat/models'; +import { startBroker } from '@rocket.chat/network-broker'; +import { Hono } from 'hono'; + +import { config } from './config'; + +export function handleFederationRoutesRegistration(app: Hono, homeserverRoutes: RouteDefinition[]): Hono { + console.info(`Registering ${homeserverRoutes.length} homeserver routes`); + + for (const route of homeserverRoutes) { + const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'; + + app[method](route.path, async (c) => { + try { + const context = { + req: c.req, + res: c.res, + params: c.req.param(), + query: c.req.query(), + body: await c.req.json().catch(() => ({})), + }; + + const result = await route.handler(context as unknown as RouteContext); + + return c.json(result); + } catch (error) { + console.error(`Error handling route ${method.toUpperCase()} ${route.path}:`, error); + return c.json({ error: 'Internal server error' }, 500); + } + }); + } + + return app; +} + +function handleHealthCheck(app: Hono) { + app.get('/health', async (c) => { + try { + return c.json({ status: 'ok' }); + } catch (err) { + console.error('Service not healthy', err); + return c.json({ status: 'not healthy' }, 500); + } + }); +} + +(async () => { + console.log('Starting federation-service on microservice mode'); + + const { db } = await getConnection(); + registerServiceModels(db, await getTrashCollection()); + + api.setBroker(startBroker()); + + const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); + const federationMatrix = new FederationMatrix(); + api.registerService(federationMatrix); + + const app = new Hono(); + handleFederationRoutesRegistration(app, federationMatrix.getAllRoutes()); + handleHealthCheck(app); + + serve({ + fetch: app.fetch, + port: config.port, + }); + + await api.start(); +})(); 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 From 153551c6a87710b9ba9305674e20d8f2123af0cf Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 30 Jun 2025 22:11:31 -0300 Subject: [PATCH 02/99] creates federation-matrix package --- ee/packages/federation-matrix/.eslintrc.json | 4 + ee/packages/federation-matrix/.gitignore | 1 + ee/packages/federation-matrix/babel.config.js | 3 + ee/packages/federation-matrix/jest.config.ts | 6 + ee/packages/federation-matrix/package.json | 46 ++++++ .../federation-matrix/src/FederationMatrix.ts | 143 ++++++++++++++++++ .../federation-matrix/src/events/index.ts | 10 ++ .../federation-matrix/src/events/message.ts | 20 +++ .../federation-matrix/src/events/ping.ts | 8 + .../federation-matrix/tsconfig.build.json | 21 +++ ee/packages/federation-matrix/tsconfig.json | 12 ++ packages/core-services/src/index.ts | 5 + .../src/types/IFederationMatrixService.ts | 20 +++ packages/model-typings/src/index.ts | 4 +- ...oomModel.ts => IMatrixBridgedRoomModel.ts} | 0 ...serModel.ts => IMatrixBridgedUserModel.ts} | 0 packages/models/src/index.ts | 4 + 17 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 ee/packages/federation-matrix/.eslintrc.json create mode 100644 ee/packages/federation-matrix/.gitignore create mode 100644 ee/packages/federation-matrix/babel.config.js create mode 100644 ee/packages/federation-matrix/jest.config.ts create mode 100644 ee/packages/federation-matrix/package.json create mode 100644 ee/packages/federation-matrix/src/FederationMatrix.ts create mode 100644 ee/packages/federation-matrix/src/events/index.ts create mode 100644 ee/packages/federation-matrix/src/events/message.ts create mode 100644 ee/packages/federation-matrix/src/events/ping.ts create mode 100644 ee/packages/federation-matrix/tsconfig.build.json create mode 100644 ee/packages/federation-matrix/tsconfig.json create mode 100644 packages/core-services/src/types/IFederationMatrixService.ts rename packages/model-typings/src/models/{IMatrixBridgeRoomModel.ts => IMatrixBridgedRoomModel.ts} (100%) rename packages/model-typings/src/models/{IMatrixBridgeUserModel.ts => IMatrixBridgedUserModel.ts} (100%) 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..c18c8ae02465c --- /dev/null +++ b/ee/packages/federation-matrix/jest.config.ts @@ -0,0 +1,6 @@ +import server from '@rocket.chat/jest-presets/server'; +import type { Config } from 'jest'; + +export default { + preset: server.preset, +} satisfies Config; diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json new file mode 100644 index 0000000000000..e9069b1fc0331 --- /dev/null +++ b/ee/packages/federation-matrix/package.json @@ -0,0 +1,46 @@ +{ + "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/apps-engine": "workspace:^", + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@types/node": "~22.14.0", + "babel-jest": "~30.0.0", + "eslint": "~8.45.0", + "jest": "~30.0.0", + "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/homeserver": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", + "mongodb": "6.10.0", + "pino": "8.21.0" + } +} diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts new file mode 100644 index 0000000000000..2c27595f29886 --- /dev/null +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -0,0 +1,143 @@ +import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/homeserver'; +import { setupHomeserver, getAllRoutes, getAllServices } from '@rocket.chat/homeserver'; +import { Logger } from '@rocket.chat/logger'; +import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; + +import { registerEvents } from './events'; + +export class FederationMatrix extends ServiceClass implements IFederationMatrixService { + protected name = 'federation-matrix'; + + private eventHandler: Emitter; + + private homeserverServices: HomeserverServices; + + private matrixDomain: string; + + private readonly logger = new Logger(this.name); + + constructor(emitter?: Emitter) { + super(); + this.eventHandler = emitter || new Emitter(); + } + + async created(): Promise { + try { + setupHomeserver({ emitter: this.eventHandler }); + registerEvents(this.eventHandler); + } catch (error) { + this.logger.warn('Homeserver module not available, running in limited mode'); + } + } + + async getMatrixDomain(): Promise { + if (this.matrixDomain) { + return this.matrixDomain; + } + + const port = await Settings.get('Federation_Service_Matrix_Port'); + const domain = await Settings.get('Federation_Service_Matrix_Domain'); + + this.matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; + + return this.matrixDomain; + } + + async started(): Promise { + this.homeserverServices = getAllServices(); + } + + getAllRoutes() { + return getAllRoutes(); + } + + async createRoom(room: IRoom, owner: IUser, members: string[]): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room creation'); + return; + } + + try { + const matrixDomain = await this.getMatrixDomain(); + const matrixUserId = `@${owner.username}:${matrixDomain}`; + const roomName = room.name || room.fname || 'Untitled Room'; + const canonicalAlias = room.fname ? `#${room.fname}:${matrixDomain}` : undefined; + + const matrixRoomResult = await this.homeserverServices.room.createRoom( + matrixUserId, + matrixUserId, + roomName, + canonicalAlias, + canonicalAlias, + ); + + this.logger.debug('Matrix room created:', matrixRoomResult); + + await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, matrixDomain); + + await MatrixBridgedUser.createOrUpdateByLocalId(owner._id, matrixUserId, true, matrixDomain); + + for await (const member of members) { + if (member === owner.username) { + continue; + } + + const localUserId = await Users.findOneByUsername(member); + if (localUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, true, matrixDomain); + } + + // 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, roomName); + } + + this.logger.debug('Room creation completed successfully', room._id); + } catch (error) { + this.logger.error('Failed to create room:', error); + throw error; + } + } + + async sendMessage(message: IMessage, room: IRoom, user: IUser): Promise { + try { + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${room._id}`); + } + + const matrixDomain = await this.getMatrixDomain(); + const matrixUserId = `@${user.username}:${matrixDomain}`; + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); + if (!existingMatrixUserId) { + const port = await Settings.get('Federation_Service_Matrix_Port'); + const domain = await Settings.get('Federation_Service_Matrix_Domain'); + const matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain); + } + + // TODO: We should fix this to not hardcode neither inform the target server + // This is on the homeserver mandate to track all the eligible servers in the federated room + const targetServer = 'hs1-garim.tunnel.dev.rocket.chat'; + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping message send'); + return; + } + + const result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, matrixUserId, targetServer); + + // TODO: Store the event ID mapping for future reference (edits, deletions, etc.) + // This would allow us to map between Rocket.Chat message IDs and Matrix event IDs + + this.logger.debug('Message sent to Matrix successfully:', result.event_id); + } catch (error) { + this.logger.error('Failed to send message to Matrix:', error); + throw 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..e259893a9d15b --- /dev/null +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -0,0 +1,10 @@ +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; + +import { message } from './message'; +import { ping } from './ping'; + +export function registerEvents(emitter: Emitter) { + ping(emitter); + message(emitter); +} 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..6bfb02b01a228 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -0,0 +1,20 @@ +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; + +export function message(emitter: Emitter) { + emitter.on('homeserver.matrix.message', async (data) => { + console.log('Received Matrix message event:', { + event_id: data.event_id, + room_id: data.room_id, + sender: data.sender, + body: data.content.body, + }); + + // await Message.receiveMessageFromFederation({ + // fromId: data.sender, + // rid: data.room_id, + // msg: data.content.body, + // federation_event_id: data.event_id, + // }); + }); +} 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..c50ea5698ff03 --- /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/homeserver'; + +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/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/src/index.ts b/packages/core-services/src/index.ts index d8b4e6e671cdb..00a75c3b02f27 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -11,6 +11,7 @@ import type { ICalendarService } from './types/ICalendarService'; import type { IDeviceManagementService } from './types/IDeviceManagementService'; import type { IEnterpriseSettings } from './types/IEnterpriseSettings'; import type { IFederationService, IFederationServiceEE } from './types/IFederationService'; +import type { IFederationMatrixService } from './types/IFederationMatrixService'; import type { IImportService } from './types/IImportService'; import type { ILDAPService } from './types/ILDAPService'; import type { ILicense } from './types/ILicense'; @@ -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'); \ No newline at end of file diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts new file mode 100644 index 0000000000000..54d2920c41d51 --- /dev/null +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -0,0 +1,20 @@ +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; + +export interface IRouteContext { + params: any; + query: any; + body: any; + headers: Record; + setStatus: (code: number) => void; + setHeader: (key: string, value: string) => void; +} + +export interface IFederationMatrixService { + getAllRoutes(): { + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + path: string; + handler: (ctx: IRouteContext) => Promise; + }[]; + createRoom(room: IRoom, owner: IUser, members: string[]): Promise; + sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; +} diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 69184ad32e247..68faceaba46aa 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -69,8 +69,8 @@ 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/IMatrixBridgedRoomModel'; +export * from './models/IMatrixBridgedUserModel'; 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/IMatrixBridgedRoomModel.ts similarity index 100% rename from packages/model-typings/src/models/IMatrixBridgeRoomModel.ts rename to packages/model-typings/src/models/IMatrixBridgedRoomModel.ts diff --git a/packages/model-typings/src/models/IMatrixBridgeUserModel.ts b/packages/model-typings/src/models/IMatrixBridgedUserModel.ts similarity index 100% rename from packages/model-typings/src/models/IMatrixBridgeUserModel.ts rename to packages/model-typings/src/models/IMatrixBridgedUserModel.ts diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 89c2136d3668a..2aa03d57b725e 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -119,6 +119,8 @@ import { TeamRaw, UsersRaw, UsersSessionsRaw, + MatrixBridgedUserRaw, + MatrixBridgedRoomRaw, } from './modelClasses'; import { proxify, registerModel } from './proxify'; @@ -262,6 +264,8 @@ export function registerServiceModels(db: Db, trash?: Collection new LivechatRoomsRaw(db)); registerModel('IUploadsModel', () => new UploadsRaw(db)); registerModel('ILivechatVisitorsModel', () => new LivechatVisitorsRaw(db)); + registerModel('IMatrixBridgedUserModel', () => new MatrixBridgedUserRaw(db)); + registerModel('IMatrixBridgedRoomModel', () => new MatrixBridgedRoomRaw(db)); } if (!dbWatchersDisabled) { From c2835afcbe7eec98e2653a85f6e6799b882f4a10 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 30 Jun 2025 22:17:52 -0300 Subject: [PATCH 03/99] creates monolith API for federation --- apps/meteor/app/api/server/api.ts | 16 + apps/meteor/ee/server/api/federation.ts | 141 +++ apps/meteor/package.json | 2 + apps/meteor/server/services/startup.ts | 7 + packages/core-services/src/index.ts | 4 +- yarn.lock | 1468 ++++++++++++++++++++++- 6 files changed, 1632 insertions(+), 6 deletions(-) create mode 100644 apps/meteor/ee/server/api/federation.ts diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index dbafa020706f8..7138e187d404d 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -11,6 +11,7 @@ import { metricsMiddleware } from './middlewares/metrics'; import { remoteAddressMiddleware } from './middlewares/remoteAddressMiddleware'; import { tracerSpanMiddleware } from './middlewares/tracer'; import { type APIActionHandler, RocketChatAPIRouter } from './router'; +import { isRunningMs } from '../../../server/lib/isRunningMs'; import { metrics } from '../../metrics/server'; import { settings } from '../../settings/server'; @@ -42,6 +43,9 @@ const createApi = function _createApi(options: { version?: string; useDefaultAut export const API: { api: Router<'/api', any, APIActionHandler>; v1: APIClass<'/v1'>; + _matrix: Router<'/_matrix', any, APIActionHandler>; + wellKnown: Router<'/.well-known', any, APIActionHandler>; + matrixInternal: Router<'/internal', any, APIActionHandler>; default: APIClass; ApiClass: typeof APIClass; channels?: { @@ -73,6 +77,9 @@ export const API: { version: 'v1', useDefaultAuth: true, }), + _matrix: new RocketChatAPIRouter('/_matrix'), + wellKnown: new RocketChatAPIRouter('/.well-known'), + matrixInternal: new RocketChatAPIRouter('/internal'), default: createApi({}), }; @@ -100,6 +107,15 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => }); export const startRestAPI = () => { + // Register federation routes at root level if enabled and not running in MS mode + if (settings.get('Federation_Service_Enabled') && !isRunningMs()) { + (WebApp.rawConnectHandlers as unknown as ReturnType) + .use(API._matrix.router) + .use(API.wellKnown.router) + .use(API.matrixInternal.router); + } + + // Register main API routes under /api prefix (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api .use(remoteAddressMiddleware) diff --git a/apps/meteor/ee/server/api/federation.ts b/apps/meteor/ee/server/api/federation.ts new file mode 100644 index 0000000000000..8a9ca6803669e --- /dev/null +++ b/apps/meteor/ee/server/api/federation.ts @@ -0,0 +1,141 @@ +import type { IFederationMatrixService } from '@rocket.chat/core-services'; +import { Logger } from '@rocket.chat/logger'; + +import { API } from '../../../app/api/server'; +import { isRunningMs } from '../../../server/lib/isRunningMs'; + +interface IExtendedContext { + urlParams?: Record; + queryParams?: Record; + bodyParams?: Record; + request?: Request; + _statusCode?: number; + _headers?: Record; +} + +const logger = new Logger('FederationRoutes'); + +export async function registerFederationRoutes(federationService: IFederationMatrixService): Promise { + if (isRunningMs()) { + return; + } + + try { + const routes = federationService.getAllRoutes(); + + for (const route of routes) { + const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'; + + let router: any; + if (route.path.startsWith('/_matrix')) { + router = API._matrix; + } else if (route.path.startsWith('/.well-known')) { + router = API.wellKnown; + } else if (route.path.startsWith('/internal')) { + router = API.matrixInternal; + } else { + logger.error(`Unknown route prefix for path: ${route.path}`); + continue; + } + + if (method === 'patch') { + if (typeof (router as any).method === 'function') { + const routePath = route.path.replace(/^\/_matrix|^\/\.well-known|^\/internal/, ''); + (router as any).method('PATCH', routePath || '/', { response: {} }, async function (this: IExtendedContext) { + try { + const context = { + params: this.urlParams || {}, + query: this.queryParams || {}, + body: this.bodyParams || {}, + headers: this.request?.headers ? Object.fromEntries(this.request.headers.entries()) : {}, + setStatus: (code: number) => { + this._statusCode = code; + }, + setHeader: (key: string, value: string) => { + if (!this._headers) { + this._headers = {}; + } + this._headers[key] = value; + }, + }; + + const response = await route.handler(context); + + const result: any = { + statusCode: this._statusCode || 200, + body: response, + }; + + if (this._headers) { + result.headers = this._headers; + } + + return result; + } catch (error) { + logger.error(`Error handling route: ${route.path}`, error); + return { + statusCode: 500, + body: { error: 'Internal server error' }, + }; + } + }); + continue; + } else { + logger.error(`Cannot register PATCH method for route ${route.path} - method() function not available`); + continue; + } + } + + if (typeof (router as any)[method] !== 'function') { + logger.error(`Method ${method} not found on router for path: ${route.path}`); + continue; + } + + const routePath = route.path.replace(/^\/_matrix|^\/\.well-known|^\/internal/, ''); + + (router as any)[method](routePath || '/', { response: {} }, async function (this: IExtendedContext) { + try { + const context = { + params: this.urlParams || {}, + query: this.queryParams || {}, + body: this.bodyParams || {}, + headers: this.request?.headers ? Object.fromEntries(this.request.headers.entries()) : {}, + setStatus: (code: number) => { + this._statusCode = code; + }, + setHeader: (key: string, value: string) => { + if (!this._headers) { + this._headers = {}; + } + this._headers[key] = value; + }, + }; + + const response = await route.handler(context); + + const result: any = { + statusCode: this._statusCode || 200, + body: response, + }; + + if (this._headers) { + result.headers = this._headers; + } + + return result; + } catch (error) { + logger.error(`Error handling route: ${route.path}`, error); + return { + statusCode: 500, + body: { error: 'Internal server error' }, + }; + } + }); + } + + logger.log('[Federation] Registered', routes.length, 'federation routes'); + } catch (error) { + logger.error('[Federation] Failed to register routes:', error); + throw error; + } +} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 116806ccda552..cc470c8759435 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -252,6 +252,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", diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 2a334932d9530..5ad3e82c6baf5 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -31,6 +31,7 @@ import { UploadService } from './upload/service'; import { UserService } from './user/service'; import { VideoConfService } from './video-conference/service'; import { VoipAsteriskService } from './voip-asterisk/service'; +import { registerFederationRoutes } from '../../ee/server/api/federation'; import { i18n } from '../lib/i18n'; export const registerServices = async (): Promise => { @@ -73,6 +74,12 @@ export const registerServices = async (): Promise => { api.registerService(new Presence()); api.registerService(new Authorization()); + // TODO: Add it to a proper place since it's EE only + const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); + const federationMatrix = new FederationMatrix(); + api.registerService(federationMatrix); + await registerFederationRoutes(federationMatrix); + // Run EE services defined outside of the main repo // Otherwise, monolith would ignore them :( // Always register the service and manage licensing inside the service (tbd) diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 00a75c3b02f27..2d7e4b9a96081 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -10,8 +10,8 @@ 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 { IFederationService, IFederationServiceEE } from './types/IFederationService'; 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'; import type { ILicense } from './types/ILicense'; @@ -196,4 +196,4 @@ export const User = proxify('user'); // of service/method not available export const EnterpriseSettings = proxify('ee-settings'); -export const FederationMatrix = proxify('federation-matrix'); \ No newline at end of file +export const FederationMatrix = proxify('federation-matrix'); diff --git a/yarn.lock b/yarn.lock index 6e9e582970af7..02aee1dc2856c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,9 +1,6 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - __metadata: version: 8 - cacheKey: 10 + cacheKey: merged "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 @@ -118,6 +115,33 @@ __metadata: languageName: node linkType: hard +"@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.26.2, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -1550,6 +1574,22 @@ __metadata: languageName: node linkType: hard +"@babel/regjsgen@npm:^0.8.0": + version: 0.8.0 + resolution: "@babel/regjsgen@npm:0.8.0" + checksum: 10/c57fb730b17332b7572574b74364a77d70faa302a281a62819476fa3b09822974fd75af77aea603ad77378395be64e81f89f0e800bf86cbbf21652d49ce12ee8 + 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.2, @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.6.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": + version: 7.24.4 + resolution: "@babel/runtime@npm:7.24.4" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308 + 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": version: 7.26.10 resolution: "@babel/runtime@npm:7.26.10" @@ -1559,6 +1599,44 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.21.0": + version: 7.25.9 + resolution: "@babel/runtime@npm:7.25.9" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/8d904cfcb433374b3bb90369452751c94ae69547cdd3679950de4527ac5d04195b9c4a1840482a6f3a84694cb22a6403a7f98b826d60cd945918223a4a6b479c + languageName: node + linkType: hard + +"@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/runtime@npm:~7.26.0": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/parser": "npm:^7.22.15" + "@babel/types": "npm:^7.22.15" + checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f + 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": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -1570,6 +1648,39 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2": + version: 7.27.2 + resolution: "@babel/template@npm:7.27.2" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/parser": "npm:^7.27.2" + "@babel/types": "npm:^7.27.1" + checksum: 10/fed15a84beb0b9340e5f81566600dbee5eccd92e4b9cc42a944359b1aa1082373391d9d5fc3656981dff27233ec935d0bc96453cf507f60a4b079463999244d8 + languageName: node + linkType: hard + +"@babel/template@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/template@npm:7.25.7" + dependencies: + "@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.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" @@ -1585,6 +1696,54 @@ __metadata: 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/7431614d76d4a053e429208db82f2846a415833f3d9eb2e11ef72eeb3c64dfd71f4a4d983de1a4a047b36165a1f5a64de8ca2a417534cc472005c740ffcb9c6a + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.25.7": + 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 + +"@babel/traverse@npm:^7.22.20": + version: 7.23.5 + resolution: "@babel/traverse@npm:7.23.5" + dependencies: + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/parser": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" + debug: "npm:^4.1.0" + globals: "npm:^11.1.0" + checksum: 10/281cae2765caad88c7af6214eab3647db0e9cadc7ffcd3fd924f09fbb9bd09d97d6fb210794b7545c317ce417a30016636530043a455ba6922349e39c1ba622a + languageName: node + linkType: hard + "@babel/traverse@npm:^7.23.2": version: 7.28.0 resolution: "@babel/traverse@npm:7.28.0" @@ -1600,6 +1759,51 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.26.7": + version: 7.26.7 + resolution: "@babel/traverse@npm:7.26.7" + dependencies: + "@babel/code-frame": "npm:^7.26.2" + "@babel/generator": "npm:^7.26.5" + "@babel/parser": "npm:^7.26.7" + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.26.7" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/c821c9682fe0b9edf7f7cbe9cc3e0787ffee3f73b52c13b21b463f8979950a6433f5e7e482a74348d22c0b7a05180e6f72b23eb6732328b49c59fc6388ebf6e5 + 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/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/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": version: 7.27.6 resolution: "@babel/types@npm:7.27.6" @@ -1610,6 +1814,68 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + 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, @babel/types@npm:^7.25.8": + 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, @babel/types@npm:^7.26.0": + 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.5, @babel/types@npm:^7.26.7": + version: 7.26.7 + resolution: "@babel/types@npm:7.26.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/2264efd02cc261ca5d1c5bc94497c8995238f28afd2b7483b24ea64dd694cf46b00d51815bf0c87f0d0061ea221569c77893aeecb0d4b4bb254e9c2f938d7669 + 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.27.3, @babel/types@npm:^7.27.6": + version: 7.27.6 + resolution: "@babel/types@npm:7.27.6" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10/174741c667775680628a09117828bbeffb35ea543f59bf80649d0d60672f7815a0740ddece3cca87516199033a039166a6936434131fce2b6a820227e64f91ae + languageName: node + linkType: hard + "@babel/types@npm:^7.28.0": version: 7.28.0 resolution: "@babel/types@npm:7.28.0" @@ -1627,6 +1893,97 @@ __metadata: languageName: node linkType: hard +"@biomejs/biome@npm:^1.9.4": + version: 1.9.4 + resolution: "@biomejs/biome@npm:1.9.4" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:1.9.4" + "@biomejs/cli-darwin-x64": "npm:1.9.4" + "@biomejs/cli-linux-arm64": "npm:1.9.4" + "@biomejs/cli-linux-arm64-musl": "npm:1.9.4" + "@biomejs/cli-linux-x64": "npm:1.9.4" + "@biomejs/cli-linux-x64-musl": "npm:1.9.4" + "@biomejs/cli-win32-arm64": "npm:1.9.4" + "@biomejs/cli-win32-x64": "npm:1.9.4" + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 10/bd8ff8fb4dc0581bd60a9b9ac28d0cd03ba17c6a1de2ab6228b7fda582079594ceee774f47e41aac2fc6d35de1637def2e32ef2e58fa24e22d1b24ef9ee5cefa + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-darwin-arm64@npm:1.9.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-darwin-x64@npm:1.9.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-arm64-musl@npm:1.9.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-arm64@npm:1.9.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-x64-musl@npm:1.9.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-linux-x64@npm:1.9.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-win32-arm64@npm:1.9.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:1.9.4": + version: 1.9.4 + resolution: "@biomejs/cli-win32-x64@npm:1.9.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@blakek/curry@npm:^2.0.2": version: 2.0.2 resolution: "@blakek/curry@npm:2.0.2" @@ -1644,6 +2001,26 @@ __metadata: languageName: node linkType: hard +"@bogeychan/elysia-etag@npm:^0.0.6": + version: 0.0.6 + resolution: "@bogeychan/elysia-etag@npm:0.0.6" + peerDependencies: + elysia: ">= 1.0.22" + checksum: 10/7f21b03f0c12f66762618eb5fc4440d775e2369c3caedfa8f6eb801d6f4ebc347a18859b3745647de4d14016da30b69795397119c07b620489eebf28e875848b + languageName: node + linkType: hard + +"@bogeychan/elysia-logger@npm:^0.1.4": + version: 0.1.8 + resolution: "@bogeychan/elysia-logger@npm:0.1.8" + dependencies: + pino: "npm:^9.6.0" + peerDependencies: + elysia: ">= 1.2.10" + checksum: 10/2d81b9c7e8d094254d1bb71a48fede3e89d8da232127517645dccc1663ff0fd7259da73c09b0cb511fb003d958b51db930af481e1638a1e278ca3b1641484555 + languageName: node + linkType: hard + "@bugsnag/browser@npm:^7.20.2": version: 7.20.2 resolution: "@bugsnag/browser@npm:7.20.2" @@ -2183,6 +2560,20 @@ __metadata: languageName: node linkType: hard +"@elysiajs/swagger@npm:^1.3.0": + version: 1.3.0 + resolution: "@elysiajs/swagger@npm:1.3.0" + dependencies: + "@scalar/themes": "npm:^0.9.52" + "@scalar/types": "npm:^0.0.12" + openapi-types: "npm:^12.1.3" + pathe: "npm:^1.1.2" + peerDependencies: + elysia: ">= 1.3.0" + checksum: 10/b065c8d20e8056f75ad04d736c3f7f17a0b851b9bbeb4df85be3ea188120e294dc3b082b877c6aff35cdd409f526f420625a464128553379494e60bea3c5e32f + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.3": version: 1.4.3 resolution: "@emnapi/core@npm:1.4.3" @@ -2623,6 +3014,56 @@ __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 + +"@hs/core@workspace:*, @hs/core@workspace:homeserver/packages/core": + version: 0.0.0-use.local + resolution: "@hs/core@workspace:homeserver/packages/core" + dependencies: + bun-types: "npm:latest" + elysia: "npm:latest" + ts-node: "npm:^10.9.2" + ts-patch: "npm:^3.1.2" + typescript: "npm:^5.4.2" + typia: "npm:^5.5.7" + languageName: unknown + linkType: soft + +"@hs/federation-sdk@workspace:homeserver/packages/federation-sdk": + version: 0.0.0-use.local + resolution: "@hs/federation-sdk@workspace:homeserver/packages/federation-sdk" + dependencies: + "@nestjs/common": "npm:^11.1.1" + "@nestjs/core": "npm:^11.1.1" + reflect-metadata: "npm:^0.2.2" + rxjs: "npm:^7.8.2" + tsyringe: "npm:^4.10.0" + tweetnacl: "npm:^1.0.3" + zod: "npm:^3.22.4" + peerDependencies: + typescript: ^5.0.0 + languageName: unknown + linkType: soft + +"@hs/homeserver@workspace:homeserver/packages/homeserver": + version: 0.0.0-use.local + resolution: "@hs/homeserver@workspace:homeserver/packages/homeserver" + dependencies: + "@hs/core": "workspace:*" + bun-types: "npm:latest" + mongodb: "npm:^6.16.0" + nats: "npm:^2.29.3" + tsyringe: "npm:^4.10.0" + languageName: unknown + linkType: soft + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -3676,6 +4117,13 @@ __metadata: languageName: node linkType: hard +"@lukeed/csprng@npm:^1.0.0": + version: 1.1.0 + resolution: "@lukeed/csprng@npm:1.1.0" + checksum: 10/926f5f7fc629470ca9a8af355bfcd0271d34535f7be3890f69902432bddc3262029bb5dbe9025542cf6c9883d878692eef2815fc2f3ba5b92e9da1f9eba2e51b + languageName: node + linkType: hard + "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -3937,6 +4385,64 @@ __metadata: languageName: node linkType: hard +"@nestjs/common@npm:^11.1.1": + version: 11.1.3 + resolution: "@nestjs/common@npm:11.1.3" + dependencies: + file-type: "npm:21.0.0" + iterare: "npm:1.2.1" + load-esm: "npm:1.0.2" + tslib: "npm:2.8.1" + uid: "npm:2.0.2" + peerDependencies: + class-transformer: ">=0.4.1" + class-validator: ">=0.13.2" + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + checksum: 10/b1c9699cdf9cee96ed0178e5eed3b1829c210bbdbd703bb9e1e0def2c7456273d73329635439b16164e43de8a8d54dd5b7d1dd5dba65417d746bd731b8c5d685 + languageName: node + linkType: hard + +"@nestjs/core@npm:^11.1.1": + version: 11.1.3 + resolution: "@nestjs/core@npm:11.1.3" + dependencies: + "@nuxt/opencollective": "npm:0.4.1" + fast-safe-stringify: "npm:2.1.1" + iterare: "npm:1.2.1" + path-to-regexp: "npm:8.2.0" + tslib: "npm:2.8.1" + uid: "npm:2.0.2" + peerDependencies: + "@nestjs/common": ^11.0.0 + "@nestjs/microservices": ^11.0.0 + "@nestjs/platform-express": ^11.0.0 + "@nestjs/websockets": ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + "@nestjs/microservices": + optional: true + "@nestjs/platform-express": + optional: true + "@nestjs/websockets": + optional: true + checksum: 10/b46a9f877170f7e96429da5970684525277bcb341f3a24032b6748ad503b5c7164b77410daf9c0f3ca99b5f6b99985d5eb739501cfc01c941218397f32f2d095 + 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" @@ -4287,6 +4793,17 @@ __metadata: languageName: node linkType: hard +"@nuxt/opencollective@npm:0.4.1": + version: 0.4.1 + resolution: "@nuxt/opencollective@npm:0.4.1" + dependencies: + consola: "npm:^3.2.3" + bin: + opencollective: bin/opencollective.js + checksum: 10/37739657e87196c7f1019a76bc33dc6e33b028eeeec43ffbf29c821e89bf5c170514e9e224456e1da85d95859ba63a3a36bd7ce1b82f2d366f7be3d6299e7631 + languageName: node + linkType: hard + "@octokit/auth-token@npm:^4.0.0": version: 4.0.0 resolution: "@octokit/auth-token@npm:4.0.0" @@ -7124,6 +7641,27 @@ __metadata: languageName: unknown linkType: soft +"@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: + "@rocket.chat/apps-engine": "workspace:^" + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/icons": "npm:^0.43.0" + "@rocket.chat/message-parser": "workspace:^" + "@rocket.chat/ui-kit": "workspace:~" + "@types/express": "npm:^4.17.23" + eslint: "npm:~8.45.0" + mongodb: "npm:6.10.0" + npm-run-all: "npm:~4.1.5" + prettier: "npm:~3.3.3" + rimraf: "npm:^6.0.1" + ts-patch: "npm:^3.3.0" + typescript: "npm:~5.9.2" + typia: "npm:~9.7.0" + languageName: unknown + linkType: soft + "@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" @@ -7263,6 +7801,13 @@ __metadata: languageName: unknown linkType: soft +"@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 + languageName: node + linkType: hard + "@rocket.chat/emitter@npm:~0.31.25": version: 0.31.25 resolution: "@rocket.chat/emitter@npm:0.31.25" @@ -7302,6 +7847,57 @@ __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/apps-engine": "workspace:^" + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": "npm:^0.31.25" + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/homeserver": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@types/node": "npm:~22.14.0" + babel-jest: "npm:~30.0.0" + eslint: "npm:~8.45.0" + jest: "npm:~30.0.0" + mongodb: "npm:6.10.0" + pino: "npm:8.21.0" + typescript: "npm:~5.8.3" + languageName: unknown + linkType: soft + +"@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/homeserver": "workspace:*" + "@rocket.chat/http-router": "workspace:*" + "@rocket.chat/models": "workspace:*" + "@types/bun": "npm:latest" + "@types/express": "npm:^4.17.17" + hono: "npm:^3.11.0" + pino: "npm:^8.16.0" + polka: "npm:^0.5.2" + reflect-metadata: "npm:^0.2.2" + tsyringe: "npm:^4.10.0" + typescript: "npm:^5.3.0" + zod: "npm:^3.22.0" + 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,6 +8149,30 @@ __metadata: languageName: unknown linkType: soft +"@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: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/eslint-config": "workspace:~" + "@rocket.chat/jest-presets": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/tsconfig": "workspace:*" + "@types/express": "npm:^4.17.23" + "@types/jest": "npm:~30.0.0" + "@types/supertest": "npm:^6.0.3" + ajv: "npm:^8.17.1" + eslint: "npm:~8.45.0" + express: "npm:^4.21.2" + hono: "npm:^4.6.19" + jest: "npm:~30.0.5" + qs: "npm:^6.14.0" + supertest: "npm:^7.1.1" + ts-jest: "npm:~29.4.0" + typescript: "npm:~5.9.2" + languageName: unknown + linkType: soft + "@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" @@ -8357,6 +8977,28 @@ __metadata: languageName: unknown linkType: soft +"@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: + "@rocket.chat/jest-presets": "workspace:~" + "@rocket.chat/model-typings": "workspace:~" + "@rocket.chat/random": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/sha256": "workspace:^" + "@rocket.chat/string-helpers": "npm:^0.31.25" + "@rocket.chat/tracing": "workspace:^" + "@rocket.chat/tsconfig": "workspace:*" + "@types/jest": "npm:~30.0.0" + "@types/node-rsa": "npm:^1.1.4" + date-fns: "npm:~4.1.0" + eslint: "npm:~8.45.0" + jest: "npm:~30.0.5" + node-rsa: "npm:^1.1.1" + typescript: "npm:~5.9.2" + languageName: unknown + linkType: soft + "@rocket.chat/models@workspace:^, @rocket.chat/models@workspace:packages/models": version: 0.0.0-use.local resolution: "@rocket.chat/models@workspace:packages/models" @@ -9615,6 +10257,13 @@ __metadata: languageName: node linkType: hard +"@samchon/openapi@npm:^4.3.1": + version: 4.3.3 + resolution: "@samchon/openapi@npm:4.3.3" + checksum: 10/a25f66c2735dba223f50f9ff4767494aec401bde895f41982f58c7c1856c5c9f2567ddb26eb5228851a9552767dbe7e9d2032c1946a4c90bba01887216dbde8f + languageName: node + linkType: hard + "@samchon/openapi@npm:^4.7.1": version: 4.7.1 resolution: "@samchon/openapi@npm:4.7.1" @@ -9622,6 +10271,54 @@ __metadata: languageName: node linkType: hard +"@scalar/openapi-types@npm:0.1.1": + version: 0.1.1 + resolution: "@scalar/openapi-types@npm:0.1.1" + checksum: 10/d9ad8d3c8846c0ce1525dc945c4e147989a6234cd4f7d00bb15420f82da2f7f6f38f112193bc5e62e4f9791f62704f04b0fdcb2a905c133ec2c86fa791eefdfb + languageName: node + linkType: hard + +"@scalar/openapi-types@npm:0.2.0": + version: 0.2.0 + resolution: "@scalar/openapi-types@npm:0.2.0" + dependencies: + zod: "npm:^3.23.8" + checksum: 10/ee42c8125ebe8ca61bd369124eff2006f83f7b0572413596cfd0eb65aa0c07abb43b6bfbcdaa8fac29139d391fefdff0e029798b352e00e687e07dd69259e79f + languageName: node + linkType: hard + +"@scalar/themes@npm:^0.9.52": + version: 0.9.86 + resolution: "@scalar/themes@npm:0.9.86" + dependencies: + "@scalar/types": "npm:0.1.7" + checksum: 10/683f108624b608358c9f8aefb18a679e059c84f2a98d2ff71ad66a1131fa8ecedc9d2f071ca0d065b7c9b29c350631414e1a1c82b5cf2a5bc06e72849caffd6f + languageName: node + linkType: hard + +"@scalar/types@npm:0.1.7": + version: 0.1.7 + resolution: "@scalar/types@npm:0.1.7" + dependencies: + "@scalar/openapi-types": "npm:0.2.0" + "@unhead/schema": "npm:^1.11.11" + nanoid: "npm:^5.1.5" + type-fest: "npm:^4.20.0" + zod: "npm:^3.23.8" + checksum: 10/cf9a117d960dbfba6187c2e44a73eafb4368d9be85ab6ebe33ebaeda57f10bdac65b53a946afa3de97e8e18b5190a7ea08827ed32709701ac719116df509e661 + languageName: node + linkType: hard + +"@scalar/types@npm:^0.0.12": + version: 0.0.12 + resolution: "@scalar/types@npm:0.0.12" + dependencies: + "@scalar/openapi-types": "npm:0.1.1" + "@unhead/schema": "npm:^1.9.5" + checksum: 10/596fe35b9e8b1823cf72aa7ffbd4998200e09136fc53e4b00b573c9a2c7fa6bf8905e00d5b344337eeef0db02eae0a69e0b6a34ceefdc355ccbc764de9f4b274 + languageName: node + linkType: hard + "@scarf/scarf@npm:=1.4.0": version: 1.4.0 resolution: "@scarf/scarf@npm:1.4.0" @@ -9750,6 +10447,13 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.34.33": + version: 0.34.37 + resolution: "@sinclair/typebox@npm:0.34.37" + checksum: 10/bd2ba20a9f7446a353719bc0e6dfab75a13e47af6470fb792e418c585a4eb3bae4f806f87e4067efe2fb0c7686de11e6cf11823a1fe13660892e51cefcfceaea + languageName: node + linkType: hard + "@sindresorhus/is@npm:^0.7.0": version: 0.7.0 resolution: "@sindresorhus/is@npm:0.7.0" @@ -9793,6 +10497,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^13.0.0, @sinonjs/fake-timers@npm:^13.0.5, @sinonjs/fake-timers@npm:^13.0.1, @sinonjs/fake-timers@npm:^13.0.5": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f + languageName: node + linkType: hard + "@sinonjs/samsam@npm:^8.0.1": version: 8.0.2 resolution: "@sinonjs/samsam@npm:8.0.2" @@ -10808,6 +11521,17 @@ __metadata: languageName: node linkType: hard +"@tokenizer/inflate@npm:^0.2.7": + version: 0.2.7 + resolution: "@tokenizer/inflate@npm:0.2.7" + dependencies: + debug: "npm:^4.4.0" + fflate: "npm:^0.8.2" + token-types: "npm:^6.0.0" + checksum: 10/6cee1857e47ca0fc053d6cd87773b7c21857ab84cb847c7d9437a76d923e265c88f8e99a4ac9643c2f989f4b9791259ca17128f0480191449e2b412821a1b9a7 + languageName: node + linkType: hard + "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -10967,6 +11691,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,6 +12248,18 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.0.6 + resolution: "@types/express-serve-static-core@npm:5.0.6" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/9dc51bdee7da9ad4792e97dd1be5b3071b5128f26d3b87a753070221bb36c8f9d16074b95a8b972acc965641e987b1e279a44675e7312ac8f3e18ec9abe93940 + 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": version: 4.17.23 resolution: "@types/express@npm:4.17.23" @@ -11527,6 +12272,41 @@ __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/express@npm:^4.17.17": + version: 4.17.23 + resolution: "@types/express@npm:4.17.23" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/cf4d540bbd90801cdc79a46107b8873404698a7fd0c3e8dd42989d52d3bd7f5b8768672e54c20835e41e27349c319bb47a404ad14c0f8db0e9d055ba1cb8a05b + languageName: node + linkType: hard + +"@types/express@npm:^5.0.1": + version: 5.0.3 + resolution: "@types/express@npm:5.0.3" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^5.0.0" + "@types/serve-static": "npm:*" + checksum: 10/bb6f10c14c8e3cce07f79ee172688aa9592852abd7577b663cd0c2054307f172c2b2b36468c918fed0d4ac359b99695807b384b3da6157dfa79acbac2226b59b + languageName: node + linkType: hard + "@types/fibers@npm:^3.1.4": version: 3.1.4 resolution: "@types/fibers@npm:3.1.4" @@ -11534,6 +12314,15 @@ __metadata: languageName: node linkType: hard +"@types/gc-stats@npm:^1.4.3": + version: 1.4.3 + resolution: "@types/gc-stats@npm:1.4.3" + dependencies: + "@types/node": "npm:*" + checksum: 10/983ad3841a1f62a1014e4c0becf81d258f07dc44849598079168df6f4ea293eb8abef7896b6847b9dd68e61b4628525279950b3bb4a75c045b8d461cbaaef3e9 + languageName: node + linkType: hard + "@types/geojson@npm:*": version: 7946.0.10 resolution: "@types/geojson@npm:7946.0.10" @@ -12044,6 +12833,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.15.18": + version: 22.15.33 + resolution: "@types/node@npm:22.15.33" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10/5734cbca7fc363f3d6ad191e1be645cc9885d642e9f90688892459f10629cf663d2206e7ed7b255dd476baaa86fb011aa09647e77520958b5993b391f793856f + languageName: node + linkType: hard + "@types/nodemailer@npm:*, @types/nodemailer@npm:^6.4.17": version: 6.4.17 resolution: "@types/nodemailer@npm:6.4.17" @@ -12331,6 +13129,15 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^17.0.4": + version: 17.0.4 + resolution: "@types/sinon@npm:17.0.4" + dependencies: + "@types/sinonjs__fake-timers": "npm:*" + checksum: 10/286c34e66e3573673ba59a332ac81189e20dd591c5c5360c8ff3ed83a59a60bdb1d4c8f13ab8863a4d5ce636282e4b11c640b87f398663eee152988ca09b1933 + languageName: node + linkType: hard + "@types/sinonjs__fake-timers@npm:*": version: 8.1.2 resolution: "@types/sinonjs__fake-timers@npm:8.1.2" @@ -12525,6 +13332,13 @@ __metadata: languageName: node linkType: hard +"@types/validator@npm:^13.11.8": + version: 13.15.2 + resolution: "@types/validator@npm:13.15.2" + checksum: 10/0d6e349329359c6781b1a6b4f48349fe3221b655041887bdf9e3e3c3508716106f3fe00db9a33002288e5a4b5abdcc49d7128d5b1d3edcfac11bda7aa696b34d + languageName: node + linkType: hard + "@types/wait-on@npm:^5.2.0": version: 5.3.4 resolution: "@types/wait-on@npm:5.3.4" @@ -12946,6 +13760,16 @@ __metadata: languageName: node linkType: hard +"@unhead/schema@npm:^1.11.11, @unhead/schema@npm:^1.9.5": + version: 1.11.20 + resolution: "@unhead/schema@npm:1.11.20" + dependencies: + hookable: "npm:^5.5.3" + zhead: "npm:^2.2.4" + checksum: 10/150b35c25368c476a2fce030d7b17ff52a3694c7863c288e1ad17ddf1aa0a97314c0732a381b477f9805772d3edb7b492238cd3f1201514b760f521447b239e1 + languageName: node + linkType: hard + "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0": version: 1.9.0 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0" @@ -13712,6 +14536,27 @@ __metadata: languageName: node linkType: hard +"amqp-connection-manager@npm:^4.1.14": + version: 4.1.14 + resolution: "amqp-connection-manager@npm:4.1.14" + dependencies: + promise-breaker: "npm:^6.0.0" + peerDependencies: + amqplib: "*" + checksum: 10/502edfd40b9c26eeac0f094fb6603d106370790e1c343554a975a9174651f0fa35e4232f9fd26c418cd6604f1800f5849506166bf15fd61efe3762b08ea65dcc + languageName: node + linkType: hard + +"amqplib@npm:^0.10.8": + version: 0.10.8 + resolution: "amqplib@npm:0.10.8" + dependencies: + buffer-more-ints: "npm:~1.0.0" + url-parse: "npm:~1.5.10" + checksum: 10/63f4ca383d76746746f4c2559062caa08de201fdb5d8c3263582500ae43eb07c2053ffe1c1a0bd4c615ddf67655d9e632646f7bc43e71cc99a66f76138c47202 + languageName: node + linkType: hard + "another-json@npm:^0.2.0": version: 0.2.0 resolution: "another-json@npm:0.2.0" @@ -15393,6 +16238,13 @@ __metadata: languageName: node linkType: hard +"buffer-more-ints@npm:~1.0.0": + version: 1.0.0 + resolution: "buffer-more-ints@npm:1.0.0" + checksum: 10/603a7f35793426c8efd733eb716c2c3bf3e2f5bab95ca13ba31546d89ead3636586479c5a0d8438dd015115361a3b09b1b37ddabc170b6d42bc6c6dc2554dc61 + languageName: node + linkType: hard + "buffer-xor@npm:^1.0.3": version: 1.0.3 resolution: "buffer-xor@npm:1.0.3" @@ -15467,6 +16319,33 @@ __metadata: languageName: node linkType: hard +"bun-bagel@npm:^1.1.0": + version: 1.2.0 + resolution: "bun-bagel@npm:1.2.0" + peerDependencies: + typescript: ^5.0.0 + checksum: 10/6a68566dc3e8fc5ef81dea612a8d58268fd3df903c1fc41726e176a9e3215c725112924b422ac8aa9d986f7fe16793c1cb9840fef40b60a1ea4cf192180c1d88 + 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 + +"bun-types@npm:latest": + version: 1.2.17 + resolution: "bun-types@npm:1.2.17" + dependencies: + "@types/node": "npm:*" + checksum: 10/20f8a1fe7cb1375db22947da6ed68178058ac5895259e8ebb89b934540d523621786e831825931cf0aec87239674d839d2c108dab0ca7f258acd7a9539be5aff + languageName: node + linkType: hard + "bundle-name@npm:^4.1.0": version: 4.1.0 resolution: "bundle-name@npm:4.1.0" @@ -16150,6 +17029,24 @@ __metadata: languageName: node linkType: hard +"class-transformer@npm:^0.5.1": + version: 0.5.1 + resolution: "class-transformer@npm:0.5.1" + checksum: 10/750327e3e9a5cf233c5234252f4caf6b06c437bf68a24acbdcfb06c8e0bfff7aa97c30428184813e38e08111b42871f20c5cf669ea4490f8ae837c09f08b31e7 + languageName: node + linkType: hard + +"class-validator@npm:^0.14.2": + version: 0.14.2 + resolution: "class-validator@npm:0.14.2" + dependencies: + "@types/validator": "npm:^13.11.8" + libphonenumber-js: "npm:^1.11.1" + validator: "npm:^13.9.0" + checksum: 10/37fbbc2ddb335993bf6bbe3fcaa55ddb03e31dccdf6413753e7323e1f106fed888d298b1ecc3b2000d40096e63ee983790c2155056d0a404629bb22b31b051e0 + languageName: node + linkType: hard + "classcat@npm:^5.0.3, classcat@npm:^5.0.4": version: 5.0.4 resolution: "classcat@npm:5.0.4" @@ -16478,6 +17375,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^6.1.0, commander@npm:^6.2.0": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e + languageName: node + linkType: hard + "commander@npm:^7.2.0": version: 7.2.0 resolution: "commander@npm:7.2.0" @@ -16625,6 +17529,13 @@ __metadata: languageName: node linkType: hard +"consola@npm:^3.2.3": + version: 3.4.2 + resolution: "consola@npm:3.4.2" + checksum: 10/32192c9f50d7cac27c5d7c4ecd3ff3679aea863e6bf5bd6a9cc2b05d1cd78addf5dae71df08c54330c142be8e7fbd46f051030129b57c6aacdd771efe409c4b2 + languageName: node + linkType: hard + "console-browserify@npm:^1.1.0, console-browserify@npm:^1.2.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" @@ -16727,6 +17638,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.2": + version: 1.0.2 + resolution: "cookie@npm:1.0.2" + checksum: 10/f5817cdc84d8977761b12549eba29435e675e65c7fef172bc31737788cd8adc83796bf8abe6d950554e7987325ad2d9ac2971c5bd8ff0c4f81c145f82e4ab1be + languageName: node + linkType: hard + "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -17655,6 +18573,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.0": + version: 4.4.1 + resolution: "debug@npm:4.4.1" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + languageName: node + linkType: hard + "debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" @@ -18460,6 +19390,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.5.0": + version: 16.5.0 + resolution: "dotenv@npm:16.5.0" + checksum: 10/e68a16834f1a41cc2dfb01563bc150668ad675e6cd09191211467b5c0806b6ecd6ec438e021aa8e01cd0e72d2b70ef4302bec7cc0fe15b6955f85230b62dc8a9 + languageName: node + linkType: hard + "download@npm:^6.2.2": version: 6.2.5 resolution: "download@npm:6.2.5" @@ -18668,6 +19605,30 @@ __metadata: languageName: node linkType: hard +"elysia@npm:^1.1.26, elysia@npm:latest": + version: 1.3.5 + resolution: "elysia@npm:1.3.5" + dependencies: + "@sinclair/typebox": "npm:^0.34.33" + cookie: "npm:^1.0.2" + exact-mirror: "npm:0.1.2" + fast-decode-uri-component: "npm:^1.0.1" + openapi-types: "npm:^12.1.3" + peerDependencies: + "@sinclair/typebox": ">= 0.34.0" + exact-mirror: ">= 0.0.9" + file-type: ">= 20.0.0" + openapi-types: ">= 12.0.0" + typescript: ">= 5.0.0" + dependenciesMeta: + "@sinclair/typebox": + optional: true + openapi-types: + optional: true + checksum: 10/162bd82651bd42f52b2530e007ec921887f6b2f957bb9ab1418d59d0590a573ecc16ea2f8333558da8c8d50dafc4c73dfcf5b4113e02002ea2d42e856ebba415 + languageName: node + linkType: hard + "email-validator@npm:^2.0.4": version: 2.0.4 resolution: "email-validator@npm:2.0.4" @@ -19808,6 +20769,18 @@ __metadata: languageName: node linkType: hard +"exact-mirror@npm:0.1.2": + version: 0.1.2 + resolution: "exact-mirror@npm:0.1.2" + peerDependencies: + "@sinclair/typebox": ^0.34.15 + peerDependenciesMeta: + "@sinclair/typebox": + optional: true + checksum: 10/424f5b04605207198d38ba1ef15a577ec6688ebde19afac35ba90f2e01283a31fa30eb4b03ebcafb1c863530d63ef644da092a588ef158503e706fa654078b69 + languageName: node + linkType: hard + "exec-buffer@npm:^3.0.0, exec-buffer@npm:^3.2.0": version: 3.2.0 resolution: "exec-buffer@npm:3.2.0" @@ -20112,6 +21085,20 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10/97e1022e2aaa27acf4a986d679310bfd66bfb87fe8da9dd33b698e3e50189484001cf1eeb9670e19b59d9d299828ed86c8da354c954f125995ab2a6331c5f290 + languageName: node + linkType: hard + +"fast-decode-uri-component@npm:^1.0.1": + version: 1.0.1 + resolution: "fast-decode-uri-component@npm:1.0.1" + checksum: 10/4b6ed26974414f688be4a15eab6afa997bad4a7c8605cb1deb928b28514817b4523a1af0fa06621c6cbfedb7e5615144c2c3e7512860e3a333a31a28d537dca7 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -20174,6 +21161,13 @@ __metadata: languageName: node linkType: hard +"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -20323,6 +21317,18 @@ __metadata: languageName: node linkType: hard +"file-type@npm:21.0.0": + version: 21.0.0 + resolution: "file-type@npm:21.0.0" + dependencies: + "@tokenizer/inflate": "npm:^0.2.7" + strtok3: "npm:^10.2.2" + token-types: "npm:^6.0.0" + uint8array-extras: "npm:^1.4.0" + checksum: 10/6980e8b0ef870a98b51ab2eac5db94a1884de8476fe49dc02d2f7e0c1d1d7d44d42b6c59e67867ae90f321ddf4edd00fcfda01821591e2fa05385d0e438a9dc1 + languageName: node + linkType: hard + "file-type@npm:5.2.0, file-type@npm:^5.2.0": version: 5.2.0 resolution: "file-type@npm:5.2.0" @@ -20920,6 +21926,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" @@ -21817,6 +22830,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 + languageName: node + linkType: hard + "hepburn@npm:^1.2.0": version: 1.2.0 resolution: "hepburn@npm:1.2.0" @@ -21885,6 +22905,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" @@ -21892,6 +22919,13 @@ __metadata: languageName: node linkType: hard +"hookable@npm:^5.5.3": + version: 5.5.3 + resolution: "hookable@npm:5.5.3" + checksum: 10/c6cec06f693e99a8f8ebd55592efc68042b472a4a04522dde384620d9a2cd7f422003357bf5688525f4bb14454bb0e4188a26db847fb1f1e06875958dfc61cde + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -22280,6 +23314,15 @@ __metadata: languageName: node linkType: hard +"husky@npm:^9.1.7": + version: 9.1.7 + resolution: "husky@npm:9.1.7" + bin: + husky: bin.js + checksum: 10/c2412753f15695db369634ba70f50f5c0b7e5cb13b673d0826c411ec1bd9ddef08c1dad89ea154f57da2521d2605bd64308af748749b27d08c5f563bcd89975f + languageName: node + linkType: hard + "hyperdyperid@npm:^1.2.0": version: 1.2.0 resolution: "hyperdyperid@npm:1.2.0" @@ -23670,6 +24713,13 @@ __metadata: languageName: node linkType: hard +"iterare@npm:1.2.1": + version: 1.2.1 + resolution: "iterare@npm:1.2.1" + checksum: 10/ee8322dd9d92e86d8653c899df501c58c5b8e90d6767cf2af0b6d6dc5a4b9b7ed8bce936976f4f4c3a55be110a300c8a7d71967d03f72e104e8db66befcfd874 + languageName: node + linkType: hard + "iterate-iterator@npm:^1.0.1": version: 1.0.2 resolution: "iterate-iterator@npm:1.0.2" @@ -25190,6 +26240,24 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:^8.5.1": + version: 8.5.1 + resolution: "jsonwebtoken@npm:8.5.1" + dependencies: + jws: "npm:^3.2.2" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^5.6.0" + checksum: 10/a7b52ea570f70bea183ceca970c003f223d9d3425d72498002e9775485c7584bfa3751d1c7291dbb59738074cba288effe73591b87bec5d467622ab3a156fdb6 + languageName: node + linkType: hard + "jsprim@npm:^1.2.2": version: 1.4.2 resolution: "jsprim@npm:1.4.2" @@ -25497,6 +26565,13 @@ __metadata: languageName: node linkType: hard +"libphonenumber-js@npm:^1.11.1": + version: 1.12.9 + resolution: "libphonenumber-js@npm:1.12.9" + checksum: 10/9ec49349ccc68b40fe46e7aca3335e203261d194c780fd297c2f4eade5c9e3745197efccdafe9bc9ec5d650663c94bd464c6c256f3a2fa3f53f140c54470a6e5 + languageName: node + linkType: hard + "libqp@npm:2.1.1": version: 2.1.1 resolution: "libqp@npm:2.1.1" @@ -25570,6 +26645,13 @@ __metadata: languageName: node linkType: hard +"load-esm@npm:1.0.2": + version: 1.0.2 + resolution: "load-esm@npm:1.0.2" + checksum: 10/1b4adb40c28c6fdbd4ca8c97942c04debddb3c93ae91413540ff5a21ca3511a651988c835cb80cad7288d1ecb869c4794b8a787ab02e09cc07ec951ad1eefcf9 + languageName: node + linkType: hard + "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -27278,6 +28360,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": version: 3.3.8 resolution: "nanoid@npm:3.3.8" @@ -27287,6 +28378,31 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.8": + version: 3.3.8 + resolution: "nanoid@npm:3.3.8" + bin: + nanoid: bin/nanoid.js + checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 + languageName: node + linkType: hard + +"nanoid@npm:^5.1.5": + version: 5.1.5 + resolution: "nanoid@npm:5.1.5" + bin: + nanoid: bin/nanoid.js + checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 + languageName: node + linkType: hard + +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 10/276feb8e30189fe18718e85b6f82e4f952822baa2e7696f771cc42571a235b789dc5907a14d9ffb6838c3e4ff4c25717c2575e5ce1cf6e02e496e204c11e57f6 + languageName: node + linkType: hard + "napi-build-utils@npm:^2.0.0": version: 2.0.0 resolution: "napi-build-utils@npm:2.0.0" @@ -27312,6 +28428,15 @@ __metadata: languageName: node linkType: hard +"nats@npm:^2.29.3": + version: 2.29.3 + resolution: "nats@npm:2.29.3" + dependencies: + nkeys.js: "npm:1.1.0" + checksum: 10/c60b0c61a23afa3412e16c95a486a19c1a48544a9de4eb7004eaddba06f2f341f6aa475924158045b851cba6755ed4d46832b894b45d9c7342df674aa6e2b7ff + languageName: node + linkType: hard + "natural-compare-lite@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare-lite@npm:1.4.0" @@ -27525,6 +28650,15 @@ __metadata: languageName: node linkType: hard +"node-jsonwebtoken@npm:^0.0.1": + version: 0.0.1 + resolution: "node-jsonwebtoken@npm:0.0.1" + dependencies: + jsonwebtoken: "npm:^8.5.1" + checksum: 10/d3b85a996409e2900bbb1c6cf0ee02e4df1f3344ac722f31bad0f5ed0569dfe0d14a8be0189dee46e96096a97719b1bf57dacee44284b390153ffd7fbb1fba62 + languageName: node + linkType: hard + "node-noop@npm:^0.0.1": version: 0.0.1 resolution: "node-noop@npm:0.0.1" @@ -28072,6 +29206,13 @@ __metadata: languageName: node linkType: hard +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 10/9d1d7ed848622b63d0a4c3f881689161b99427133054e46b8e3241e137f1c78bb0031c5d80b420ee79ac2e91d2e727ffd6fc13c553d1b0488ddc8ad389dcbef8 + languageName: node + linkType: hard + "opentracing@npm:^0.14.4": version: 0.14.7 resolution: "opentracing@npm:0.14.7" @@ -28786,6 +29927,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:8.2.0, path-to-regexp@npm:^8.1.0": + version: 8.2.0 + resolution: "path-to-regexp@npm:8.2.0" + checksum: 10/23378276a172b8ba5f5fb824475d1818ca5ccee7bbdb4674701616470f23a14e536c1db11da9c9e6d82b82c556a817bbf4eee6e41b9ed20090ef9427cbb38e13 + languageName: node + linkType: hard + "path-to-regexp@npm:^6.3.0": version: 6.3.0 resolution: "path-to-regexp@npm:6.3.0" @@ -28837,6 +29985,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 10/f201d796351bf7433d147b92c20eb154a4e0ea83512017bf4ec4e492a5d6e738fb45798be4259a61aa81270179fce11026f6ff0d3fa04173041de044defe9d80 + languageName: node + linkType: hard + "pathington@npm:^1.1.7": version: 1.1.7 resolution: "pathington@npm:1.1.7" @@ -29022,6 +30177,38 @@ __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:^13.0.0": + version: 13.0.0 + resolution: "pino-pretty@npm:13.0.0" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pump: "npm:^3.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 10/9861fdbe88db000e3b0fe959f0fb7b5913e8d16af70373155d48854c5d509629e7e1ba09ed3fac24a9bd2729451567a698938b9741d84de63eb549843450e71c + languageName: node + linkType: hard + "pino-pretty@npm:^7.6.1": version: 7.6.1 resolution: "pino-pretty@npm:7.6.1" @@ -29052,6 +30239,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:8.21.0": + version: 8.21.0 + resolution: "pino@npm:8.21.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:^1.2.0" + pino-std-serializers: "npm:^6.0.0" + process-warning: "npm:^3.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:^3.7.0" + thread-stream: "npm:^2.6.0" + bin: + pino: bin.js + checksum: 10/5a054eab533ab91b20f63497b86070f0a6b40e4688cde9de66d23e03d6046c4e95d69c3f526dea9f30bcbc5874c7fbf0f91660cded4753946fd02261ca8ac340 + languageName: node + linkType: hard + "pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" @@ -30169,6 +31377,13 @@ __metadata: languageName: node linkType: hard +"promise-breaker@npm:^6.0.0": + version: 6.0.0 + resolution: "promise-breaker@npm:6.0.0" + checksum: 10/6f7ad5e55d3f434dc1e02907c3294dc4a44f9962d9af9de186095c75c8f76d11feeb927e96ec9e177fcc9209690defb5c64eeac4767e7c3dd4f120e9d14fb0c8 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -31321,6 +32536,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" @@ -32071,6 +33293,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:^7.5.5, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d + languageName: node + linkType: hard + "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -32393,6 +33624,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10/7a24cffcaa13f53c09ce55e05efe25cd41328730b2308678624f8b9f5fc3093fc4d189f47950f0b811ff8f3c3039c24a2c36717ba7961615c682045bf03e1dda + languageName: node + linkType: hard + "semver@npm:~5.3.0": version: 5.3.0 resolution: "semver@npm:5.3.0" @@ -32805,6 +34045,19 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^20.0.0": + version: 20.0.0 + resolution: "sinon@npm:20.0.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + "@sinonjs/fake-timers": "npm:^13.0.5" + "@sinonjs/samsam": "npm:^8.0.1" + diff: "npm:^7.0.0" + supports-color: "npm:^7.2.0" + checksum: 10/825cb36a58c0510cec03d9bef4fe66a12baf0e0cfdf1600423e3da1e6d57a03fe8161f4859340ea13d4c42e63da1724a260ef4c5ce119dc9ee075ad93b6e8bdd + languageName: node + linkType: hard + "sip-methods@npm:^0.3.0": version: 0.3.0 resolution: "sip-methods@npm:0.3.0" @@ -32833,6 +34086,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 +34223,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" @@ -33797,6 +35066,15 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^10.2.2": + version: 10.3.1 + resolution: "strtok3@npm:10.3.1" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + checksum: 10/bb7950cc9ce98ec742a5db360630f0b004f16197959ae28d8c8dad4f8f0e405d71cfdc992483038ba29a0b4cbd7227618ad2492005b510d84a3fc5903df0c13f + languageName: node + linkType: hard + "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -34684,6 +35962,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^6.0.0": + version: 6.0.0 + resolution: "token-types@npm:6.0.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 10/b541b605d602e8e6495745badb35f90ee8f997e43dc29bc51aee7e9a0bc3c6bc7372a305bd45f3e80d75223c2b6a5c7e65cb5159d8c4e49fa25cdbaae531fad4 + languageName: node + linkType: hard + "tough-cookie@npm:^2.3.3, tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -34931,6 +36219,23 @@ __metadata: languageName: node linkType: hard +"ts-patch@npm:^3.1.2, ts-patch@npm:^3.3.0": + version: 3.3.0 + resolution: "ts-patch@npm:3.3.0" + dependencies: + chalk: "npm:^4.1.2" + global-prefix: "npm:^4.0.0" + minimist: "npm:^1.2.8" + resolve: "npm:^1.22.2" + semver: "npm:^7.6.3" + strip-ansi: "npm:^6.0.1" + bin: + ts-patch: bin/ts-patch.js + tspc: bin/tspc.js + checksum: 10/5b0a42cacdfef2136b18b785b4ca6579d470001560d42139057fccf6ed85a622a73e5efba1b20426b047bf9aaf2090a23a9afca4e72ab330b166344dd45b3386 + languageName: node + linkType: hard + "ts-patch@npm:^3.3.0": version: 3.3.0 resolution: "ts-patch@npm:3.3.0" @@ -34978,6 +36283,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.8.1": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 + languageName: node + linkType: hard + "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -34985,6 +36297,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.8.1, tslib@npm:^1.9.3": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb + languageName: node + linkType: hard + "tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": version: 2.7.0 resolution: "tslib@npm:2.7.0" @@ -35010,6 +36329,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" @@ -35118,6 +36446,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:1.0.3, tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 + languageName: node + linkType: hard + "tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" @@ -35216,6 +36551,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.20.0, type-fest@npm:^4.41.0": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 + languageName: node + linkType: hard + "type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" @@ -35319,6 +36661,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.3.0, typescript@npm:^5.4.2, typescript@npm:^5.8.3, typescript@npm:~5.9.2": + version: 5.9.2 + resolution: "typescript@npm:5.9.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/cc2fe6c822819de5d453fa25aa9f32096bf70dde215d481faa1ad84a283dfb264e33988ed8f6d36bc803dd0b16dbe943efa311a798ef76d5b3892a05dfbfd628 + languageName: node + linkType: hard + "typescript@npm:~5.9.2": version: 5.9.2 resolution: "typescript@npm:5.9.2" @@ -35329,6 +36681,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.3.0#optional!builtin, typescript@patch:typescript@npm%3A^5.4.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#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: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/bd810ab13e8e557225a8b5122370385440b933e4e077d5c7641a8afd207fdc8be9c346e3c678adba934b64e0e70b0acf5eef9493ea05170a48ce22bef845fdc7 + languageName: node + linkType: hard + "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" @@ -35339,6 +36701,41 @@ __metadata: languageName: node linkType: hard +"typia@npm:^5.5.7": + version: 5.5.10 + resolution: "typia@npm:5.5.10" + dependencies: + commander: "npm:^10.0.0" + comment-json: "npm:^4.2.3" + inquirer: "npm:^8.2.5" + randexp: "npm:^0.5.3" + peerDependencies: + typescript: ">=4.8.0 <5.5.0" + bin: + typia: lib/executable/typia.js + checksum: 10/d3dd44474ebd927f1aa1744812676db20fccf475d0808dae05cad8a65c60a0c4ca83cd3073dccbe32412e6eaa319c20715f775042ae42fc64d950ec1f13bc6f3 + languageName: node + linkType: hard + +"typia@npm:~9.3.1": + version: 9.3.1 + resolution: "typia@npm:9.3.1" + dependencies: + "@samchon/openapi": "npm:^4.3.1" + "@standard-schema/spec": "npm:^1.0.0" + commander: "npm:^10.0.0" + comment-json: "npm:^4.2.3" + inquirer: "npm:^8.2.5" + package-manager-detector: "npm:^0.2.0" + randexp: "npm:^0.5.3" + peerDependencies: + typescript: ">=4.8.0 <5.10.0" + bin: + typia: lib/executable/typia.js + checksum: 10/1f0b260402900b46225af7bba1110399a21a9276122a306b34448a4d6f9de12035857f520baf1ee27a663274835a85e6f9cdeb856c866e437b544b87164d48f9 + languageName: node + linkType: hard + "typia@npm:~9.7.0": version: 9.7.0 resolution: "typia@npm:9.7.0" @@ -35386,6 +36783,22 @@ __metadata: languageName: node linkType: hard +"uid@npm:2.0.2": + version: 2.0.2 + resolution: "uid@npm:2.0.2" + dependencies: + "@lukeed/csprng": "npm:^1.0.0" + checksum: 10/18f6da43d8e1b8643077e8123f877b4506759d9accc15337140a1bf7c99f299a66e88b27ab4c640e66e6a10f19e3a85afa45fdf830dd4bab7570d07a3d51e073 + languageName: node + linkType: hard + +"uint8array-extras@npm:^1.4.0": + version: 1.4.0 + resolution: "uint8array-extras@npm:1.4.0" + checksum: 10/4d2955d67c112e5ebaa4901272a75fc9ad14902c40f05a178b01e32387aa2702b6840472d931a1ca16e068ac59013c7d9ee2b4b2f141c4e73ba4bc7456490599 + languageName: node + linkType: hard + "umd@npm:^3.0.0": version: 3.0.3 resolution: "umd@npm:3.0.3" @@ -35802,6 +37215,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.10, url-parse@npm:~1.5.10": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad + languageName: node + linkType: hard + "url-to-options@npm:^1.0.1": version: 1.0.1 resolution: "url-to-options@npm:1.0.1" @@ -35911,6 +37334,13 @@ __metadata: languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: 10/5d6949693d58cb2e636a84f3ee1c6e7b2f9c16cb1d42d0ecb386d8c025c69e327205aa1c69e2868cc06a01e5e20681fbba55a4e0ed0cce913d60334024eae798 + languageName: node + linkType: hard + "utils-merge@npm:1.0.1, utils-merge@npm:^1.0.0": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" @@ -36005,6 +37435,13 @@ __metadata: languageName: node linkType: hard +"validator@npm:^13.9.0": + version: 13.15.15 + resolution: "validator@npm:13.15.15" + checksum: 10/a43d9271c879468b1ad6dd5d2597b71719a185d2c7ceb3d68f3c9c8c17c25af0d90a53edfa7efaa6ac0d4425ba0345684b9c7d8111bde0d06d915a634a287018 + languageName: node + linkType: hard + "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -37127,6 +38564,13 @@ __metadata: languageName: node linkType: hard +"zhead@npm:^2.2.4": + version: 2.2.4 + resolution: "zhead@npm:2.2.4" + checksum: 10/cfa2ba81bf936fd4f5ba19360412c7017a164250823f22e575e1956b20c73d76b989985c02a4f89e2e02f3fb203fbe8857072cf5fbece59a374d1a6bf588555b + languageName: node + linkType: hard + "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1" @@ -37138,6 +38582,22 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.24.5": + version: 3.24.5 + resolution: "zod-to-json-schema@npm:3.24.5" + peerDependencies: + zod: ^3.24.1 + checksum: 10/1af291b4c429945c9568c2e924bdb7c66ab8d139cbeb9a99b6e9fc9e1b02863f85d07759b9303714f07ceda3993dcaf0ebcb80d2c18bb2aaf5502b2c1016affd + languageName: node + linkType: hard + +"zod@npm:^3.22.0, zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.3": + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa + languageName: node + linkType: hard + "zod@npm:^3.24.1": version: 3.24.1 resolution: "zod@npm:3.24.1" From 5efcc8fea01d42f34eed52b04d3b8138faf40a50 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 30 Jun 2025 22:21:03 -0300 Subject: [PATCH 04/99] adds federation service settings --- .../server/services/federation/utils.ts | 14 ++++++++++ .../server/settings/federation-service.ts | 28 +++++++++++++++++++ apps/meteor/server/settings/index.ts | 2 ++ packages/i18n/src/locales/en.i18n.json | 8 ++++++ 4 files changed, 52 insertions(+) create mode 100644 apps/meteor/server/settings/federation-service.ts diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts index 0256b4f04fe85..d5885888dfedc 100644 --- a/apps/meteor/server/services/federation/utils.ts +++ b/apps/meteor/server/services/federation/utils.ts @@ -1,5 +1,19 @@ import { settings } from '../../../app/settings/server'; +// TODO: We should check if the federation service is ready instead of just +// checking if the matrix federation is enabled +export function getFederationVersion(): 'matrix' | 'native' | null { + if (settings.get('Federation_Matrix_enabled')) { + return 'matrix'; + } + + if (settings.get('Federation_Service_Enabled')) { + return 'native'; + } + + return null; +} + export function isFederationEnabled(): boolean { return settings.get('Federation_Matrix_enabled'); } diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts new file mode 100644 index 0000000000000..3ee05dcdd3003 --- /dev/null +++ b/apps/meteor/server/settings/federation-service.ts @@ -0,0 +1,28 @@ +import { settingsRegistry } from '../../app/settings/server'; + +export const createFederationServiceSettings = async (): Promise => { + await settingsRegistry.addGroup('Federation Service', async function () { + await this.add('Federation_Service_Enabled', false, { + type: 'boolean', + i18nLabel: 'Federation_Service_Enabled', + i18nDescription: 'Federation_Service_Enabled_Description', + public: true, + alert: 'Federation_Service_Alert', + }); + + await this.add('Federation_Service_Matrix_Domain', 'localhost', { + type: 'string', + i18nLabel: 'Federation_Service_Matrix_Domain', + i18nDescription: 'Federation_Service_Matrix_Domain_Description', + public: true, + }); + + await this.add('Federation_Service_Matrix_Port', 3000, { + type: 'int', + i18nLabel: 'Federation_Service_Matrix_Port', + i18nDescription: 'Federation_Service_Matrix_Port_Description', + public: true, + alert: 'Federation_Service_Matrix_Port_Alert', + }); + }); +}; diff --git a/apps/meteor/server/settings/index.ts b/apps/meteor/server/settings/index.ts index 2a7973eddeece..032d9ee30e0ce 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'; @@ -51,6 +52,7 @@ await Promise.all([ createEmailSettings(), createE2ESettings(), createFederationSettings(), + createFederationServiceSettings(), createFileUploadSettings(), createGeneralSettings(), createIRCSettings(), diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 54815d3ee34d3..49efee75ed9ed 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2153,6 +2153,14 @@ "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_Matrix_Domain": "Matrix domain", + "Federation_Service_Matrix_Domain_Description": "The domain of the Matrix server to use for federation.", + "Federation_Service_Matrix_Port": "Matrix port", + "Federation_Service_Matrix_Port_Description": "The port of the Matrix server to use for federation.", + "Federation_Service_Matrix_Port_Alert": "If you're using a DNS or a reverse proxy, you should set this to the port of the DNS handling the federation traffic. E.g. your server is running on port 3000 and you're using a DNS to handle incoming traffic from port 3000 to the DNS name rc1.server.com only. In this case, you should set this to 443.", + "Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", From c1477f8207292ecc8ab719a52449069094c6913d Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 30 Jun 2025 22:23:59 -0300 Subject: [PATCH 05/99] adds create federated room support --- .../app/lib/server/functions/createRoom.ts | 12 +++++++++- .../client/hooks/useIsFederationEnabled.ts | 6 +++-- .../client/lib/rooms/roomCoordinator.tsx | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 0ef03ee7b85b1..51f0485de3916 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,6 +1,6 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team } from '@rocket.chat/core-services'; +import { FederationMatrix, Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; @@ -13,6 +13,7 @@ import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../../../.. import { calculateRoomRolePriorityFromRoles } from '../../../../lib/roles/calculateRoomRolePriorityFromRoles'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority'; +import { getFederationVersion } from '../../../../server/services/federation/utils'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; @@ -68,6 +69,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); @@ -265,6 +267,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 }); } @@ -276,3 +279,10 @@ export const createRoom = async ( ...room, }; }; + +callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members }) => { + const federationVersion = getFederationVersion(); + if (federationVersion === 'matrix') { + await FederationMatrix.createRoom(room, owner, members); + } +}); 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/rooms/roomCoordinator.tsx b/apps/meteor/client/lib/rooms/roomCoordinator.tsx index 1e99fa0bfa7df..26b6afb82bc3a 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.tsx +++ b/apps/meteor/client/lib/rooms/roomCoordinator.tsx @@ -140,6 +140,28 @@ class RoomCoordinatorClient extends RoomCoordinator { return false; } + // #ToDo: Move this out of the RoomCoordinator + public archived(rid: string): boolean { + const room = Rooms.findOne({ _id: rid }, { fields: { archived: 1 } }); + return Boolean(room?.archived); + } + + public verifyCanSendMessage(rid: string): boolean { + const room = Rooms.findOne({ _id: rid }, { fields: { t: 1, federated: 1 } }); + if (!room?.t) { + return false; + } + if (!this.getRoomDirectives(room.t).canSendMessage(rid)) { + return false; + } + // TODO: Adjust this to call a central function validator instead of settings + // since there will be more than one setting to check (status, connection, etc.) + if (isRoomFederated(room)) { + return settings.get('Federation_Matrix_enabled') || settings.get('Federation_Service_Enabled'); + } + return true; + } + private validateRoute(route: IRoomTypeRouteConfig): void { const { name, path, link } = route; From 0381af994be6321df36b2aad4cfea476e7abc963 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 15 Jul 2025 10:11:37 -0300 Subject: [PATCH 06/99] chore: new federation callbacks listeners (#36421) --- .../app/lib/server/functions/createRoom.ts | 10 +---- .../ee/server/hooks/federation/index.ts | 39 +++++++++++++++++++ apps/meteor/ee/server/index.ts | 1 + 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 apps/meteor/ee/server/hooks/federation/index.ts diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 51f0485de3916..7eca89ae33761 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,6 +1,6 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { FederationMatrix, Message, Team } from '@rocket.chat/core-services'; +import { Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; @@ -13,7 +13,6 @@ import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../../../.. import { calculateRoomRolePriorityFromRoles } from '../../../../lib/roles/calculateRoomRolePriorityFromRoles'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority'; -import { getFederationVersion } from '../../../../server/services/federation/utils'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; @@ -279,10 +278,3 @@ export const createRoom = async ( ...room, }; }; - -callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members }) => { - const federationVersion = getFederationVersion(); - if (federationVersion === 'matrix') { - await FederationMatrix.createRoom(room, owner, members); - } -}); 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..b96b8a5f1d21e --- /dev/null +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -0,0 +1,39 @@ +// import { FederationMatrix } from '@rocket.chat/core-services'; + +import { FederationMatrix } from '@rocket.chat/core-services'; + +import { callbacks } from '../../../../lib/callbacks'; +import { getFederationVersion } from '../../../../server/services/federation/utils'; + +// callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); + +callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members }) => { + const federationVersion = getFederationVersion(); + if (federationVersion === 'matrix') { + await FederationMatrix.createRoom(room, owner, members); + } +}); + +callbacks.add( + 'afterSaveMessage', + async (message, { room, user }) => { + const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); + const federationVersion = getFederationVersion(); + + if (shouldBeHandledByFederation && federationVersion === 'native') { + 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, + 'federation-v2-after-room-message-sent', +); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index c9340d1f440a4..826639c03efb3 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'; From bf7f9d9077fa581a53ae189a68aa1de7490968f0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 15:05:58 -0300 Subject: [PATCH 07/99] send message --- .../server/services/messages/service.ts | 14 ++ .../federation-matrix/src/FederationMatrix.ts | 2 + .../federation-matrix/src/events/message.ts | 135 ++++++++++++++++-- .../src/types/IMessageService.ts | 11 ++ 4 files changed, 149 insertions(+), 13 deletions(-) diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 828d2da19ead4..02de25012eb70 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -85,6 +85,20 @@ export class MessageService extends ServiceClassInternal implements IMessageServ return executeSendMessage(fromId, { rid, msg }); } + async saveMessageFromFederation({ + fromId, + rid, + msg, + federation_event_id, + }: { + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + }): Promise { + return executeSendMessage(fromId, { rid, msg, federation: { eventId: federation_event_id } }); + } + async sendMessageWithValidation(user: IUser, message: Partial, room: Partial, upsert = false): Promise { return sendMessage(user, message, room, upsert); } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 2c27595f29886..989bfc7103f70 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -88,6 +88,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const localUserId = await Users.findOneByUsername(member); if (localUserId) { await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, true, matrixDomain); + continue; } // We are not generating bridged users for members outside of the current workspace @@ -98,6 +99,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug('Room creation completed successfully', room._id); } catch (error) { + console.log(error); this.logger.error('Failed to create room:', error); throw error; } diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 6bfb02b01a228..6fed94b3a802a 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,20 +1,129 @@ import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; +import { Message } from '@rocket.chat/core-services'; +import { UserStatus } from '@rocket.chat/core-typings'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions } from '@rocket.chat/models'; +import { Logger } from '@rocket.chat/logger'; + +const logger = new Logger('federation-matrix:message'); export function message(emitter: Emitter) { emitter.on('homeserver.matrix.message', async (data) => { - console.log('Received Matrix message event:', { - event_id: data.event_id, - room_id: data.room_id, - sender: data.sender, - body: data.content.body, - }); - - // await Message.receiveMessageFromFederation({ - // fromId: data.sender, - // rid: data.room_id, - // msg: data.content.body, - // federation_event_id: data.event_id, - // }); + try { + logger.info('Received Matrix message event:', { + event_id: data.event_id, + room_id: data.room_id, + sender: data.sender, + }); + + const message = data.event.content?.body?.toString(); + if (!message) { + logger.debug('No message found in event content'); + return; + } + + const [userPart, domain] = data.sender.split(':'); + if (!userPart || !domain) { + logger.error('Invalid Matrix sender ID format:', data.sender); + return; + } + const username = userPart.substring(1); + + let user = await Users.findOneByUsername(data.sender); + + if (!user) { + logger.info('Creating new federated user:', { username: data.sender, externalId: data.sender }); + + const userData: Partial = { + username: data.sender, + name: username, // TODO: Fetch display name from Matrix profile + type: 'user', + status: UserStatus.ONLINE, + active: true, + roles: ['user'], + requirePasswordChange: false, + federated: true, // Mark as federated user + createdAt: new Date(), + _updatedAt: new Date(), + }; + + const { insertedId } = await Users.insertOne(userData as IUser); + + await MatrixBridgedUser.createOrUpdateByLocalId( + insertedId, + data.sender, + true, // isRemote = true for external Matrix users + domain, + ); + + user = await Users.findOneById(insertedId); + if (!user) { + logger.error('Failed to create user:', data.sender); + return; + } + + logger.info('Successfully created federated user:', { userId: user._id, username }); + } else { + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, data.sender, true, domain); + } + + const internalRoomId = await MatrixBridgedRoom.getLocalRoomId(data.room_id); + if (!internalRoomId) { + logger.error('Room not found in bridge mapping:', data.room_id); + // TODO: Handle room creation for unknown federated rooms + return; + } + + const room = await Rooms.findOneById(internalRoomId); + if (!room) { + logger.error('Room not found:', internalRoomId); + return; + } + + if (!room.federated) { + logger.error('Room is not marked as federated:', { roomId: room._id, matrixRoomId: data.room_id }); + // TODO: Should we update the room to be federated? + } + + const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); + + if (!existingSubscription) { + logger.info('Creating subscription for federated user in room:', { userId: user._id, roomId: room._id }); + + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { + ts: new Date(), + open: false, + alert: false, + unread: 0, + userMentions: 0, + groupMentions: 0, + // Federation status is inherited from room.federated and user.federated + }); + + if (insertedId) { + logger.debug('Successfully created subscription:', insertedId); + // TODO: Import and use notifyOnSubscriptionChangedById if needed + // void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } + } + + logger.info('Saving federated message:', { + fromId: user._id, + roomId: internalRoomId, + eventId: data.event_id, + }); + + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: internalRoomId, + msg: message, + federation_event_id: data.event_id, + }); + + logger.debug('Successfully processed Matrix message'); + } catch (error) { + logger.error('Error processing Matrix message:', error); + } }); } diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 29da139ef63c8..f0c0a2df8b8b9 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -9,6 +9,17 @@ export interface IMessageService { user: Pick, extraData?: Partial, ): Promise; + saveMessageFromFederation({ + fromId, + rid, + msg, + federation_event_id, + }: { + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + }): Promise; saveSystemMessageAndNotifyUser( type: MessageTypesValues, rid: string, From 2125c0d6024d9cbcf839653739fc538fe0a94fcf Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Tue, 1 Jul 2025 16:19:50 -0300 Subject: [PATCH 08/99] feat: basic support for accept invitation from remote --- .../federation-matrix/src/events/index.ts | 2 + .../federation-matrix/src/events/invite.ts | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 ee/packages/federation-matrix/src/events/invite.ts diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index e259893a9d15b..1e972f53bed60 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -1,10 +1,12 @@ import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; +import { invite } from './invite'; import { message } from './message'; import { ping } from './ping'; export function registerEvents(emitter: Emitter) { ping(emitter); message(emitter); + invite(emitter); } diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts new file mode 100644 index 0000000000000..b2b9bc1a0c0fa --- /dev/null +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -0,0 +1,47 @@ + +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/homeserver'; +import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; + +export function invite(emitter: Emitter) { + emitter.on('homeserver.matrix.accept-invite', async (data) => { + const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); + if (!room) { + console.warn(`No bridged room found for room_id: ${data.room_id}`); + return; + } + + const localUser = await Users.findOneByUsername(data.sender); + if (localUser) { + await Room.addUserToRoom(room.rid, localUser); + return; + } + + const { insertedId } = await Users.insertOne({ + username: data.sender, + type: 'user', + status: UserStatus.ONLINE, + active: true, + roles: ['user'], + name: data.content.displayname || data.sender, + requirePasswordChange: false, + createdAt: new Date(), + _updatedAt: new Date(), + federated: true, + }); + const serverName = data.sender.split(':')[1] || 'unknown'; + const bridgedUser = await MatrixBridgedUser.findOne({ mri: data.sender }); + + if (!bridgedUser) { + await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, data.sender, true, serverName); + } + const user = await Users.findOneById(insertedId); + if (!user) { + console.warn(`User with ID ${insertedId} not found after insertion`); + return; + } + await Room.addUserToRoom(room.rid, user); + }); +} From 2610650b38ff2393549da98ae8d4c9e484f6c373 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 3 Jul 2025 09:59:58 -0300 Subject: [PATCH 09/99] x --- .../federation-matrix/src/FederationMatrix.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 989bfc7103f70..ee5c42498d1e9 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -85,10 +85,15 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS continue; } - const localUserId = await Users.findOneByUsername(member); - if (localUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, true, matrixDomain); - continue; + try { + // TODO: Check if it is external user - split domain etc + const localUserId = await Users.findOneByUsername(member); + if (localUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, true, matrixDomain); + // continue; + } + } catch (error) { + this.logger.error('Error creating or updating bridged user:', error); } // We are not generating bridged users for members outside of the current workspace From 6b576a461cac7c1a2846f6a40b183d2346b41f85 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 7 Jul 2025 10:33:11 -0300 Subject: [PATCH 10/99] refactor: use self contained DI package from federation-sdk (#36379) Co-authored-by: Marcos Defendi --- apps/meteor/server/services/startup.ts | 2 +- ee/apps/federation-service/package.json | 90 +- ee/apps/federation-service/src/service.ts | 57 +- ee/packages/federation-matrix/package.json | 5 +- .../federation-matrix/src/FederationMatrix.ts | 23 +- .../federation-matrix/src/events/index.ts | 2 +- .../federation-matrix/src/events/invite.ts | 2 +- .../federation-matrix/src/events/message.ts | 8 +- .../federation-matrix/src/events/ping.ts | 2 +- .../federation-matrix/src/setupContainers.ts | 40 + package.json | 1 + yarn.lock | 1180 ++++++----------- 12 files changed, 573 insertions(+), 839 deletions(-) create mode 100644 ee/packages/federation-matrix/src/setupContainers.ts diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 5ad3e82c6baf5..09d70e4008f32 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -76,7 +76,7 @@ export const registerServices = async (): Promise => { // TODO: Add it to a proper place since it's EE only const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); - const federationMatrix = new FederationMatrix(); + const federationMatrix = await FederationMatrix.create(); api.registerService(federationMatrix); await registerFederationRoutes(federationMatrix); diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index de94f694d90fb..411b00d1784a8 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -1,47 +1,47 @@ { - "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/homeserver": "workspace:*", - "@rocket.chat/http-router": "workspace:*", - "@rocket.chat/models": "workspace:*", - "hono": "^3.11.0", - "pino": "^8.16.0", - "polka": "^0.5.2", - "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0", - "zod": "^3.22.0" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/express": "^4.17.17", - "typescript": "^5.3.0" - }, - "keywords": [ - "rocketchat" - ], - "author": "Rocket.Chat" + "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", + "@hs/federation-sdk": "workspace:*", + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:*", + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-matrix": "workspace:^", + "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/models": "workspace:*", + "hono": "^3.11.0", + "pino": "^8.16.0", + "polka": "^0.5.2", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^4.17.17", + "typescript": "^5.3.0" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat" } diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index a5016d431e812..180dcd4cf464c 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -1,41 +1,36 @@ import 'reflect-metadata'; import { serve } from '@hono/node-server'; import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; -import type { RouteDefinition, RouteContext } from '@rocket.chat/homeserver'; +// import type { RouteDefinition, RouteContext } from '@hs/federation-sdk'; import { registerServiceModels } from '@rocket.chat/models'; import { startBroker } from '@rocket.chat/network-broker'; import { Hono } from 'hono'; import { config } from './config'; -export function handleFederationRoutesRegistration(app: Hono, homeserverRoutes: RouteDefinition[]): Hono { - console.info(`Registering ${homeserverRoutes.length} homeserver routes`); - - for (const route of homeserverRoutes) { - const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'; - - app[method](route.path, async (c) => { - try { - const context = { - req: c.req, - res: c.res, - params: c.req.param(), - query: c.req.query(), - body: await c.req.json().catch(() => ({})), - }; - - const result = await route.handler(context as unknown as RouteContext); - - return c.json(result); - } catch (error) { - console.error(`Error handling route ${method.toUpperCase()} ${route.path}:`, error); - return c.json({ error: 'Internal server error' }, 500); - } - }); - } - - return app; -} +// export function handleFederationRoutesRegistration(app: Hono, homeserverRoutes: RouteDefinition[]): Hono { +// // console.info(`Registering ${homeserverRoutes.length} homeserver routes`); +// // for (const route of homeserverRoutes) { +// // const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'; +// // app[method](route.path, async (c) => { +// // try { +// // const context = { +// // req: c.req, +// // res: c.res, +// // params: c.req.param(), +// // query: c.req.query(), +// // body: await c.req.json().catch(() => ({})), +// // }; +// // const result = await route.handler(context as unknown as RouteContext); +// // return c.json(result); +// // } catch (error) { +// // console.error(`Error handling route ${method.toUpperCase()} ${route.path}:`, error); +// // return c.json({ error: 'Internal server error' }, 500); +// // } +// // }); +// // } +// // return app; +// } function handleHealthCheck(app: Hono) { app.get('/health', async (c) => { @@ -57,11 +52,11 @@ function handleHealthCheck(app: Hono) { api.setBroker(startBroker()); const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); - const federationMatrix = new FederationMatrix(); + const federationMatrix = await FederationMatrix.create(); api.registerService(federationMatrix); const app = new Hono(); - handleFederationRoutesRegistration(app, federationMatrix.getAllRoutes()); + // handleFederationRoutesRegistration(app, federationMatrix.getAllRoutes()); handleHealthCheck(app); serve({ diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index e9069b1fc0331..f7be2f5e6c2ee 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -34,13 +34,14 @@ "extends": "../../../package.json" }, "dependencies": { + "@hs/federation-sdk": "workspace:^", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/homeserver": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/network-broker": "workspace:^", "mongodb": "6.10.0", - "pino": "8.21.0" + "pino": "8.21.0", + "reflect-metadata": "^0.2.2" } } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index ee5c42498d1e9..7aacaf32ac30b 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,12 +1,15 @@ +import 'reflect-metadata'; + +import type { HomeserverEventSignatures, HomeserverServices, DependencyContainer } from '@hs/federation-sdk'; +import { getAllServices } from '@hs/federation-sdk'; import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures, HomeserverServices } from '@rocket.chat/homeserver'; -import { setupHomeserver, getAllRoutes, getAllServices } from '@rocket.chat/homeserver'; import { Logger } from '@rocket.chat/logger'; import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; import { registerEvents } from './events'; +import { setup } from './setupContainers'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -17,16 +20,24 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS private matrixDomain: string; + private diContainer: DependencyContainer; + private readonly logger = new Logger(this.name); - constructor(emitter?: Emitter) { + private constructor(emitter?: Emitter) { super(); this.eventHandler = emitter || new Emitter(); } + static async create(emitter?: Emitter): Promise { + const instance = new FederationMatrix(emitter); + instance.diContainer = await setup(instance.eventHandler); + + return instance; + } + async created(): Promise { try { - setupHomeserver({ emitter: this.eventHandler }); registerEvents(this.eventHandler); } catch (error) { this.logger.warn('Homeserver module not available, running in limited mode'); @@ -47,11 +58,11 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } async started(): Promise { - this.homeserverServices = getAllServices(); + this.homeserverServices = getAllServices(this.diContainer); } getAllRoutes() { - return getAllRoutes(); + return []; } async createRoom(room: IRoom, owner: IUser, members: string[]): Promise { diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index 1e972f53bed60..c0c2747f3baf5 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -1,5 +1,5 @@ +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; import { invite } from './invite'; import { message } from './message'; diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index b2b9bc1a0c0fa..38281bc60d1c5 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -2,7 +2,7 @@ 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/homeserver'; +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; export function invite(emitter: Emitter) { diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 6fed94b3a802a..cc1fdba0eebf3 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,10 +1,10 @@ -import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { Message } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; -import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions } from '@rocket.chat/models'; +import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; +import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:message'); @@ -17,7 +17,7 @@ export function message(emitter: Emitter) { sender: data.sender, }); - const message = data.event.content?.body?.toString(); + const message = data.content?.body?.toString(); if (!message) { logger.debug('No message found in event content'); return; diff --git a/ee/packages/federation-matrix/src/events/ping.ts b/ee/packages/federation-matrix/src/events/ping.ts index c50ea5698ff03..d2d116374cb8a 100644 --- a/ee/packages/federation-matrix/src/events/ping.ts +++ b/ee/packages/federation-matrix/src/events/ping.ts @@ -1,5 +1,5 @@ import type { Emitter } from '@rocket.chat/emitter'; -import type { HomeserverEventSignatures } from '@rocket.chat/homeserver'; +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; export const ping = async (emitter: Emitter) => { emitter.on('homeserver.ping', async (data) => { diff --git a/ee/packages/federation-matrix/src/setupContainers.ts b/ee/packages/federation-matrix/src/setupContainers.ts new file mode 100644 index 0000000000000..7e24cbe92c989 --- /dev/null +++ b/ee/packages/federation-matrix/src/setupContainers.ts @@ -0,0 +1,40 @@ +import 'reflect-metadata'; + +import { toUnpaddedBase64 } from '@hs/core'; +import { ConfigService, createFederationContainer } from '@hs/federation-sdk'; +import type { DependencyContainer, FederationContainerOptions, HomeserverEventSignatures } from '@hs/federation-sdk'; +import { Emitter } from '@rocket.chat/emitter'; + +let container: DependencyContainer | undefined; + +export async function setup( + emitter: Emitter = new Emitter(), +): Promise { + const config = new ConfigService(); + const matrixConfig = config.getMatrixConfig(); + const serverConfig = config.getServerConfig(); + const signingKeys = await config.getSigningKey(); + const signingKey = signingKeys[0]; + + const containerOptions: FederationContainerOptions = { + emitter, + federationOptions: { + serverName: matrixConfig.serverName, + signingKey: toUnpaddedBase64(signingKey.privateKey), + signingKeyId: `ed25519:${signingKey.version}`, + timeout: 30000, + baseUrl: serverConfig.baseUrl, + }, + }; + + container = createFederationContainer(containerOptions); + + return container; +} + +export function getContainer(): DependencyContainer { + if (!container) { + throw new Error('Federation container is not initialized. Call setup() first.'); + } + return container; +} diff --git a/package.json b/package.json index 365fa33ceb69a..4820ee6726457 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "typescript": "~5.9.2" }, "workspaces": [ + "homeserver", "apps/*", "packages/*", "ee/apps/*", diff --git a/yarn.lock b/yarn.lock index 02aee1dc2856c..6be2e48aa64d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,9 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 8 - cacheKey: merged + cacheKey: 10 "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 @@ -142,7 +145,7 @@ __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/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: @@ -183,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: @@ -233,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" @@ -405,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 @@ -469,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" @@ -1436,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: @@ -1574,23 +1601,7 @@ __metadata: languageName: node linkType: hard -"@babel/regjsgen@npm:^0.8.0": - version: 0.8.0 - resolution: "@babel/regjsgen@npm:0.8.0" - checksum: 10/c57fb730b17332b7572574b74364a77d70faa302a281a62819476fa3b09822974fd75af77aea603ad77378395be64e81f89f0e800bf86cbbf21652d49ce12ee8 - 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.2, @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.6.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": - version: 7.24.4 - resolution: "@babel/runtime@npm:7.24.4" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308 - 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: @@ -1599,15 +1610,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.21.0": - version: 7.25.9 - resolution: "@babel/runtime@npm:7.25.9" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/8d904cfcb433374b3bb90369452751c94ae69547cdd3679950de4527ac5d04195b9c4a1840482a6f3a84694cb22a6403a7f98b826d60cd945918223a4a6b479c - languageName: node - linkType: hard - "@babel/runtime@npm:^7.25.6": version: 7.26.7 resolution: "@babel/runtime@npm:7.26.7" @@ -1617,38 +1619,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:~7.26.0": - version: 7.26.0 - resolution: "@babel/runtime@npm:7.26.0" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": - version: 7.22.15 - resolution: "@babel/template@npm:7.22.15" - dependencies: - "@babel/code-frame": "npm:^7.22.13" - "@babel/parser": "npm:^7.22.15" - "@babel/types": "npm:^7.22.15" - checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f - 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": - version: 7.27.2 - resolution: "@babel/template@npm:7.27.2" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.2" - "@babel/types": "npm:^7.27.1" - checksum: 10/fed15a84beb0b9340e5f81566600dbee5eccd92e4b9cc42a944359b1aa1082373391d9d5fc3656981dff27233ec935d0bc96453cf507f60a4b079463999244d8 - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2": +"@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: @@ -1681,21 +1652,6 @@ __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" - 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.16.0, @babel/traverse@npm:^7.25.9": version: 7.25.9 resolution: "@babel/traverse@npm:7.25.9" @@ -1711,7 +1667,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.25.7": +"@babel/traverse@npm:^7.18.9": version: 7.25.7 resolution: "@babel/traverse@npm:7.25.7" dependencies: @@ -1726,24 +1682,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.22.20": - version: 7.23.5 - resolution: "@babel/traverse@npm:7.23.5" - dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.5" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-hoist-variables": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.23.5" - "@babel/types": "npm:^7.23.5" - debug: "npm:^4.1.0" - globals: "npm:^11.1.0" - checksum: 10/281cae2765caad88c7af6214eab3647db0e9cadc7ffcd3fd924f09fbb9bd09d97d6fb210794b7545c317ce417a30016636530043a455ba6922349e39c1ba622a - languageName: node - linkType: hard - "@babel/traverse@npm:^7.23.2": version: 7.28.0 resolution: "@babel/traverse@npm:7.28.0" @@ -1759,18 +1697,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/traverse@npm:7.26.7" +"@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.26.2" - "@babel/generator": "npm:^7.26.5" - "@babel/parser": "npm:^7.26.7" - "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.26.7" + "@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/c821c9682fe0b9edf7f7cbe9cc3e0787ffee3f73b52c13b21b463f8979950a6433f5e7e482a74348d22c0b7a05180e6f72b23eb6732328b49c59fc6388ebf6e5 + checksum: 10/4debb80b9068a46e188e478272f3b6820e16d17e2651e82d0a0457176b0c3b2489994f0a0d6e8941ee90218b0a8a69fe52ba350c1aa66eb4c72570d6b2405f91 languageName: node linkType: hard @@ -1789,32 +1727,7 @@ __metadata: languageName: node linkType: hard -"@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/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": - version: 7.27.6 - resolution: "@babel/types@npm:7.27.6" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10/174741c667775680628a09117828bbeffb35ea543f59bf80649d0d60672f7815a0740ddece3cca87516199033a039166a6936434131fce2b6a820227e64f91ae - languageName: node - linkType: hard - -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@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: @@ -1825,7 +1738,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.18.9, @babel/types@npm:^7.25.7, @babel/types@npm:^7.25.8": +"@babel/types@npm:^7.18.9, @babel/types@npm:^7.25.7": version: 7.25.8 resolution: "@babel/types@npm:7.25.8" dependencies: @@ -1836,7 +1749,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": +"@babel/types@npm:^7.25.9": version: 7.26.0 resolution: "@babel/types@npm:7.26.0" dependencies: @@ -1846,13 +1759,13 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.26.5, @babel/types@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/types@npm:7.26.7" +"@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: - "@babel/helper-string-parser": "npm:^7.25.9" - "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/2264efd02cc261ca5d1c5bc94497c8995238f28afd2b7483b24ea64dd694cf46b00d51815bf0c87f0d0061ea221569c77893aeecb0d4b4bb254e9c2f938d7669 + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10/174741c667775680628a09117828bbeffb35ea543f59bf80649d0d60672f7815a0740ddece3cca87516199033a039166a6936434131fce2b6a820227e64f91ae languageName: node linkType: hard @@ -1866,23 +1779,23 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6": - version: 7.27.6 - resolution: "@babel/types@npm:7.27.6" +"@babel/types@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/types@npm:7.28.0" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10/174741c667775680628a09117828bbeffb35ea543f59bf80649d0d60672f7815a0740ddece3cca87516199033a039166a6936434131fce2b6a820227e64f91ae + checksum: 10/2f28b84efb5005d1e85fc3944219c284400c42aeefc1f6e10500a74fed43b3dfb4f9e349a5d6e0e3fc24f5d241c513b30ef00ede2885535ce7a0a4e111c2098e languageName: node linkType: hard -"@babel/types@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/types@npm:7.28.0" +"@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/2f28b84efb5005d1e85fc3944219c284400c42aeefc1f6e10500a74fed43b3dfb4f9e349a5d6e0e3fc24f5d241c513b30ef00ede2885535ce7a0a4e111c2098e + checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45 languageName: node linkType: hard @@ -2546,6 +2459,22 @@ __metadata: languageName: node linkType: hard +"@datastructures-js/heap@npm:^4.3.3": + version: 4.3.3 + resolution: "@datastructures-js/heap@npm:4.3.3" + checksum: 10/ffcdbf2f36c354d14deee19e41e1dda61a578c17ef3bdca6777b9b1b1e63edaae9deca3d5288ba6f04dd2c82902118c67e6d6eb3a2ab833a8475466a6ffb7457 + languageName: node + linkType: hard + +"@datastructures-js/priority-queue@npm:^6.3.3": + version: 6.3.3 + resolution: "@datastructures-js/priority-queue@npm:6.3.3" + dependencies: + "@datastructures-js/heap": "npm:^4.3.3" + checksum: 10/b69dd330189d9700c6ae73b75b730e48c68413647c7715e3c521b9f050b2e096aee4ae646f380f164066112817391b78caf40e7c1db4d882a45f80e608829225 + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -3027,28 +2956,38 @@ __metadata: version: 0.0.0-use.local resolution: "@hs/core@workspace:homeserver/packages/core" dependencies: + "@hs/crypto": "workspace:*" + "@hs/room": "workspace:*" bun-types: "npm:latest" - elysia: "npm:latest" ts-node: "npm:^10.9.2" ts-patch: "npm:^3.1.2" - typescript: "npm:^5.4.2" - typia: "npm:^5.5.7" + typescript: "npm:~5.9.2" languageName: unknown linkType: soft -"@hs/federation-sdk@workspace:homeserver/packages/federation-sdk": +"@hs/crypto@workspace:*, @hs/crypto@workspace:homeserver/packages/crypto": + version: 0.0.0-use.local + resolution: "@hs/crypto@workspace:homeserver/packages/crypto" + dependencies: + "@noble/ed25519": "npm:^3.0.0" + bun-types: "npm:latest" + languageName: unknown + linkType: soft + +"@hs/federation-sdk@workspace:*, @hs/federation-sdk@workspace:^, @hs/federation-sdk@workspace:homeserver/packages/federation-sdk": version: 0.0.0-use.local resolution: "@hs/federation-sdk@workspace:homeserver/packages/federation-sdk" dependencies: - "@nestjs/common": "npm:^11.1.1" - "@nestjs/core": "npm:^11.1.1" + "@hs/core": "workspace:*" + "@hs/room": "workspace:*" + "@rocket.chat/emitter": "npm:^0.31.25" + mongodb: "npm:^6.16.0" reflect-metadata: "npm:^0.2.2" - rxjs: "npm:^7.8.2" tsyringe: "npm:^4.10.0" tweetnacl: "npm:^1.0.3" zod: "npm:^3.22.4" peerDependencies: - typescript: ^5.0.0 + typescript: ~5.9.2 languageName: unknown linkType: soft @@ -3056,14 +2995,31 @@ __metadata: version: 0.0.0-use.local resolution: "@hs/homeserver@workspace:homeserver/packages/homeserver" dependencies: + "@bogeychan/elysia-etag": "npm:^0.0.6" + "@bogeychan/elysia-logger": "npm:^0.1.4" + "@elysiajs/swagger": "npm:^1.3.0" "@hs/core": "workspace:*" + "@hs/federation-sdk": "workspace:*" + "@hs/room": "workspace:*" + "@rocket.chat/emitter": "npm:^0.31.25" bun-types: "npm:latest" + elysia: "npm:^1.1.26" mongodb: "npm:^6.16.0" - nats: "npm:^2.29.3" tsyringe: "npm:^4.10.0" languageName: unknown linkType: soft +"@hs/room@workspace:*, @hs/room@workspace:homeserver/packages/room": + version: 0.0.0-use.local + resolution: "@hs/room@workspace:homeserver/packages/room" + dependencies: + "@datastructures-js/priority-queue": "npm:^6.3.3" + "@hs/crypto": "workspace:*" + bun-types: "npm:latest" + zod: "npm:^3.22.4" + languageName: unknown + linkType: soft + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -4117,13 +4073,6 @@ __metadata: languageName: node linkType: hard -"@lukeed/csprng@npm:^1.0.0": - version: 1.1.0 - resolution: "@lukeed/csprng@npm:1.1.0" - checksum: 10/926f5f7fc629470ca9a8af355bfcd0271d34535f7be3890f69902432bddc3262029bb5dbe9025542cf6c9883d878692eef2815fc2f3ba5b92e9da1f9eba2e51b - languageName: node - linkType: hard - "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -4385,57 +4334,6 @@ __metadata: languageName: node linkType: hard -"@nestjs/common@npm:^11.1.1": - version: 11.1.3 - resolution: "@nestjs/common@npm:11.1.3" - dependencies: - file-type: "npm:21.0.0" - iterare: "npm:1.2.1" - load-esm: "npm:1.0.2" - tslib: "npm:2.8.1" - uid: "npm:2.0.2" - peerDependencies: - class-transformer: ">=0.4.1" - class-validator: ">=0.13.2" - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true - checksum: 10/b1c9699cdf9cee96ed0178e5eed3b1829c210bbdbd703bb9e1e0def2c7456273d73329635439b16164e43de8a8d54dd5b7d1dd5dba65417d746bd731b8c5d685 - languageName: node - linkType: hard - -"@nestjs/core@npm:^11.1.1": - version: 11.1.3 - resolution: "@nestjs/core@npm:11.1.3" - dependencies: - "@nuxt/opencollective": "npm:0.4.1" - fast-safe-stringify: "npm:2.1.1" - iterare: "npm:1.2.1" - path-to-regexp: "npm:8.2.0" - tslib: "npm:2.8.1" - uid: "npm:2.0.2" - peerDependencies: - "@nestjs/common": ^11.0.0 - "@nestjs/microservices": ^11.0.0 - "@nestjs/platform-express": ^11.0.0 - "@nestjs/websockets": ^11.0.0 - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - "@nestjs/microservices": - optional: true - "@nestjs/platform-express": - optional: true - "@nestjs/websockets": - optional: true - checksum: 10/b46a9f877170f7e96429da5970684525277bcb341f3a24032b6748ad503b5c7164b77410daf9c0f3ca99b5f6b99985d5eb739501cfc01c941218397f32f2d095 - 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" @@ -4677,6 +4575,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" @@ -4793,17 +4698,6 @@ __metadata: languageName: node linkType: hard -"@nuxt/opencollective@npm:0.4.1": - version: 0.4.1 - resolution: "@nuxt/opencollective@npm:0.4.1" - dependencies: - consola: "npm:^3.2.3" - bin: - opencollective: bin/opencollective.js - checksum: 10/37739657e87196c7f1019a76bc33dc6e33b028eeeec43ffbf29c821e89bf5c170514e9e224456e1da85d95859ba63a3a36bd7ce1b82f2d366f7be3d6299e7631 - languageName: node - linkType: hard - "@octokit/auth-token@npm:^4.0.0": version: 4.0.0 resolution: "@octokit/auth-token@npm:4.0.0" @@ -7662,27 +7556,6 @@ __metadata: languageName: unknown linkType: soft -"@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: - "@rocket.chat/apps-engine": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": "npm:^0.43.0" - "@rocket.chat/message-parser": "workspace:^" - "@rocket.chat/ui-kit": "workspace:~" - "@types/express": "npm:^4.17.23" - eslint: "npm:~8.45.0" - mongodb: "npm:6.10.0" - npm-run-all: "npm:~4.1.5" - prettier: "npm:~3.3.3" - rimraf: "npm:^6.0.1" - ts-patch: "npm:^3.3.0" - typescript: "npm:~5.9.2" - typia: "npm:~9.7.0" - languageName: unknown - linkType: soft - "@rocket.chat/cron@workspace:^, @rocket.chat/cron@workspace:packages/cron": version: 0.0.0-use.local resolution: "@rocket.chat/cron@workspace:packages/cron" @@ -7808,13 +7681,6 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/emitter@npm:~0.31.25": - version: 0.31.25 - resolution: "@rocket.chat/emitter@npm:0.31.25" - checksum: 10/fee26d0200d60eadb246e4e2b40f99bbfaa6f748d11cb8fbbe350219a178630950b1ecbd6145a5dc93f8ff0298afdaef665f544f82bde7b3d0c687a298b9a1e3 - languageName: node - linkType: hard - "@rocket.chat/eslint-config@workspace:^, @rocket.chat/eslint-config@workspace:packages/eslint-config, @rocket.chat/eslint-config@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/eslint-config@workspace:packages/eslint-config" @@ -7855,12 +7721,12 @@ __metadata: "@babel/core": "npm:~7.26.0" "@babel/preset-env": "npm:~7.26.0" "@babel/preset-typescript": "npm:~7.26.0" + "@hs/federation-sdk": "workspace:^" "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/homeserver": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" @@ -7870,6 +7736,7 @@ __metadata: jest: "npm:~30.0.0" mongodb: "npm:6.10.0" pino: "npm:8.21.0" + reflect-metadata: "npm:^0.2.2" typescript: "npm:~5.8.3" languageName: unknown linkType: soft @@ -7879,11 +7746,11 @@ __metadata: resolution: "@rocket.chat/federation-service@workspace:ee/apps/federation-service" dependencies: "@hono/node-server": "npm:^1.14.4" + "@hs/federation-sdk": "workspace:*" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:*" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/homeserver": "workspace:*" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/models": "workspace:*" "@types/bun": "npm:latest" @@ -8173,30 +8040,6 @@ __metadata: languageName: unknown linkType: soft -"@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: - "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:~" - "@rocket.chat/jest-presets": "workspace:^" - "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/tsconfig": "workspace:*" - "@types/express": "npm:^4.17.23" - "@types/jest": "npm:~30.0.0" - "@types/supertest": "npm:^6.0.3" - ajv: "npm:^8.17.1" - eslint: "npm:~8.45.0" - express: "npm:^4.21.2" - hono: "npm:^4.6.19" - jest: "npm:~30.0.5" - qs: "npm:^6.14.0" - supertest: "npm:^7.1.1" - ts-jest: "npm:~29.4.0" - typescript: "npm:~5.9.2" - languageName: unknown - linkType: soft - "@rocket.chat/i18n@workspace:^, @rocket.chat/i18n@workspace:packages/i18n, @rocket.chat/i18n@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/i18n@workspace:packages/i18n" @@ -8575,6 +8418,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" @@ -8999,28 +8844,6 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/models@workspace:^, @rocket.chat/models@workspace:packages/models": - version: 0.0.0-use.local - resolution: "@rocket.chat/models@workspace:packages/models" - dependencies: - "@rocket.chat/jest-presets": "workspace:~" - "@rocket.chat/model-typings": "workspace:~" - "@rocket.chat/random": "workspace:^" - "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/sha256": "workspace:^" - "@rocket.chat/string-helpers": "npm:^0.31.25" - "@rocket.chat/tracing": "workspace:^" - "@rocket.chat/tsconfig": "workspace:*" - "@types/jest": "npm:~30.0.0" - "@types/node-rsa": "npm:^1.1.4" - date-fns: "npm:~4.1.0" - eslint: "npm:~8.45.0" - jest: "npm:~30.0.5" - node-rsa: "npm:^1.1.1" - typescript: "npm:~5.9.2" - languageName: unknown - linkType: soft - "@rocket.chat/mongo-adapter@workspace:^, @rocket.chat/mongo-adapter@workspace:packages/mongo-adapter, @rocket.chat/mongo-adapter@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/mongo-adapter@workspace:packages/mongo-adapter" @@ -10257,13 +10080,6 @@ __metadata: languageName: node linkType: hard -"@samchon/openapi@npm:^4.3.1": - version: 4.3.3 - resolution: "@samchon/openapi@npm:4.3.3" - checksum: 10/a25f66c2735dba223f50f9ff4767494aec401bde895f41982f58c7c1856c5c9f2567ddb26eb5228851a9552767dbe7e9d2032c1946a4c90bba01887216dbde8f - languageName: node - linkType: hard - "@samchon/openapi@npm:^4.7.1": version: 4.7.1 resolution: "@samchon/openapi@npm:4.7.1" @@ -10497,15 +10313,6 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^13.0.0, @sinonjs/fake-timers@npm:^13.0.5, @sinonjs/fake-timers@npm:^13.0.1, @sinonjs/fake-timers@npm:^13.0.5": - version: 13.0.5 - resolution: "@sinonjs/fake-timers@npm:13.0.5" - dependencies: - "@sinonjs/commons": "npm:^3.0.1" - checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f - languageName: node - linkType: hard - "@sinonjs/samsam@npm:^8.0.1": version: 8.0.2 resolution: "@sinonjs/samsam@npm:8.0.2" @@ -11521,17 +11328,6 @@ __metadata: languageName: node linkType: hard -"@tokenizer/inflate@npm:^0.2.7": - version: 0.2.7 - resolution: "@tokenizer/inflate@npm:0.2.7" - dependencies: - debug: "npm:^4.4.0" - fflate: "npm:^0.8.2" - token-types: "npm:^6.0.0" - checksum: 10/6cee1857e47ca0fc053d6cd87773b7c21857ab84cb847c7d9437a76d923e265c88f8e99a4ac9643c2f989f4b9791259ca17128f0480191449e2b412821a1b9a7 - languageName: node - linkType: hard - "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -12260,7 +12056,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: @@ -12284,18 +12080,6 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.17": - version: 4.17.23 - resolution: "@types/express@npm:4.17.23" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.33" - "@types/qs": "npm:*" - "@types/serve-static": "npm:*" - checksum: 10/cf4d540bbd90801cdc79a46107b8873404698a7fd0c3e8dd42989d52d3bd7f5b8768672e54c20835e41e27349c319bb47a404ad14c0f8db0e9d055ba1cb8a05b - languageName: node - linkType: hard - "@types/express@npm:^5.0.1": version: 5.0.3 resolution: "@types/express@npm:5.0.3" @@ -12314,15 +12098,6 @@ __metadata: languageName: node linkType: hard -"@types/gc-stats@npm:^1.4.3": - version: 1.4.3 - resolution: "@types/gc-stats@npm:1.4.3" - dependencies: - "@types/node": "npm:*" - checksum: 10/983ad3841a1f62a1014e4c0becf81d258f07dc44849598079168df6f4ea293eb8abef7896b6847b9dd68e61b4628525279950b3bb4a75c045b8d461cbaaef3e9 - languageName: node - linkType: hard - "@types/geojson@npm:*": version: 7946.0.10 resolution: "@types/geojson@npm:7946.0.10" @@ -12842,6 +12617,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" @@ -13332,13 +13116,6 @@ __metadata: languageName: node linkType: hard -"@types/validator@npm:^13.11.8": - version: 13.15.2 - resolution: "@types/validator@npm:13.15.2" - checksum: 10/0d6e349329359c6781b1a6b4f48349fe3221b655041887bdf9e3e3c3508716106f3fe00db9a33002288e5a4b5abdcc49d7128d5b1d3edcfac11bda7aa696b34d - languageName: node - linkType: hard - "@types/wait-on@npm:^5.2.0": version: 5.3.4 resolution: "@types/wait-on@npm:5.3.4" @@ -14536,27 +14313,6 @@ __metadata: languageName: node linkType: hard -"amqp-connection-manager@npm:^4.1.14": - version: 4.1.14 - resolution: "amqp-connection-manager@npm:4.1.14" - dependencies: - promise-breaker: "npm:^6.0.0" - peerDependencies: - amqplib: "*" - checksum: 10/502edfd40b9c26eeac0f094fb6603d106370790e1c343554a975a9174651f0fa35e4232f9fd26c418cd6604f1800f5849506166bf15fd61efe3762b08ea65dcc - languageName: node - linkType: hard - -"amqplib@npm:^0.10.8": - version: 0.10.8 - resolution: "amqplib@npm:0.10.8" - dependencies: - buffer-more-ints: "npm:~1.0.0" - url-parse: "npm:~1.5.10" - checksum: 10/63f4ca383d76746746f4c2559062caa08de201fdb5d8c3263582500ae43eb07c2053ffe1c1a0bd4c615ddf67655d9e632646f7bc43e71cc99a66f76138c47202 - languageName: node - linkType: hard - "another-json@npm:^0.2.0": version: 0.2.0 resolution: "another-json@npm:0.2.0" @@ -14601,6 +14357,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^7.0.0": + version: 7.0.0 + resolution: "ansi-escapes@npm:7.0.0" + dependencies: + environment: "npm:^1.0.0" + checksum: 10/2d0e2345087bd7ae6bf122b9cc05ee35560d40dcc061146edcdc02bc2d7c7c50143cd12a22e69a0b5c0f62b948b7bc9a4539ee888b80f5bd33cdfd82d01a70ab + languageName: node + linkType: hard + "ansi-html-community@npm:0.0.8, ansi-html-community@npm:^0.0.8": version: 0.0.8 resolution: "ansi-html-community@npm:0.0.8" @@ -14663,7 +14428,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 @@ -15293,7 +15058,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: @@ -16238,13 +16003,6 @@ __metadata: languageName: node linkType: hard -"buffer-more-ints@npm:~1.0.0": - version: 1.0.0 - resolution: "buffer-more-ints@npm:1.0.0" - checksum: 10/603a7f35793426c8efd733eb716c2c3bf3e2f5bab95ca13ba31546d89ead3636586479c5a0d8438dd015115361a3b09b1b37ddabc170b6d42bc6c6dc2554dc61 - languageName: node - linkType: hard - "buffer-xor@npm:^1.0.3": version: 1.0.3 resolution: "buffer-xor@npm:1.0.3" @@ -16319,15 +16077,6 @@ __metadata: languageName: node linkType: hard -"bun-bagel@npm:^1.1.0": - version: 1.2.0 - resolution: "bun-bagel@npm:1.2.0" - peerDependencies: - typescript: ^5.0.0 - checksum: 10/6a68566dc3e8fc5ef81dea612a8d58268fd3df903c1fc41726e176a9e3215c725112924b422ac8aa9d986f7fe16793c1cb9840fef40b60a1ea4cf192180c1d88 - languageName: node - linkType: hard - "bun-types@npm:1.2.16": version: 1.2.16 resolution: "bun-types@npm:1.2.16" @@ -16712,7 +16461,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:*, chalk@npm:^5.2.0": +"chalk@npm:*, chalk@npm:^5.2.0, chalk@npm:^5.4.1": version: 5.4.1 resolution: "chalk@npm:5.4.1" checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 @@ -17029,24 +16778,6 @@ __metadata: languageName: node linkType: hard -"class-transformer@npm:^0.5.1": - version: 0.5.1 - resolution: "class-transformer@npm:0.5.1" - checksum: 10/750327e3e9a5cf233c5234252f4caf6b06c437bf68a24acbdcfb06c8e0bfff7aa97c30428184813e38e08111b42871f20c5cf669ea4490f8ae837c09f08b31e7 - languageName: node - linkType: hard - -"class-validator@npm:^0.14.2": - version: 0.14.2 - resolution: "class-validator@npm:0.14.2" - dependencies: - "@types/validator": "npm:^13.11.8" - libphonenumber-js: "npm:^1.11.1" - validator: "npm:^13.9.0" - checksum: 10/37fbbc2ddb335993bf6bbe3fcaa55ddb03e31dccdf6413753e7323e1f106fed888d298b1ecc3b2000d40096e63ee983790c2155056d0a404629bb22b31b051e0 - languageName: node - linkType: hard - "classcat@npm:^5.0.3, classcat@npm:^5.0.4": version: 5.0.4 resolution: "classcat@npm:5.0.4" @@ -17079,6 +16810,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -17086,6 +16826,16 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-truncate@npm:4.0.0" + dependencies: + slice-ansi: "npm:^5.0.0" + string-width: "npm:^7.0.0" + checksum: 10/d5149175fd25ca985731bdeec46a55ec237475cf74c1a5e103baea696aceb45e372ac4acbaabf1316f06bd62e348123060f8191ffadfeedebd2a70a2a7fb199d + languageName: node + linkType: hard + "cli-width@npm:^3.0.0": version: 3.0.0 resolution: "cli-width@npm:3.0.0" @@ -17354,6 +17104,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^14.0.0": + version: 14.0.0 + resolution: "commander@npm:14.0.0" + checksum: 10/c05418bfc35a3e8b5c67bd9f75f5b773f386f9b85f83e70e7c926047f270929cb06cf13cd68f387dd6e7e23c6157de8171b28ba606abd3e6256028f1f789becf + languageName: node + linkType: hard + "commander@npm:^2.20.0, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -17368,13 +17125,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.1.0": - version: 6.2.1 - resolution: "commander@npm:6.2.1" - checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e - languageName: node - linkType: hard - "commander@npm:^6.1.0, commander@npm:^6.2.0": version: 6.2.1 resolution: "commander@npm:6.2.1" @@ -17529,13 +17279,6 @@ __metadata: languageName: node linkType: hard -"consola@npm:^3.2.3": - version: 3.4.2 - resolution: "consola@npm:3.4.2" - checksum: 10/32192c9f50d7cac27c5d7c4ecd3ff3679aea863e6bf5bd6a9cc2b05d1cd78addf5dae71df08c54330c142be8e7fbd46f051030129b57c6aacdd771efe409c4b2 - languageName: node - linkType: hard - "console-browserify@npm:^1.1.0, console-browserify@npm:^1.2.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" @@ -18573,18 +18316,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.4.0": - version: 4.4.1 - resolution: "debug@npm:4.4.1" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe - languageName: node - linkType: hard - "debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" @@ -19605,7 +19336,7 @@ __metadata: languageName: node linkType: hard -"elysia@npm:^1.1.26, elysia@npm:latest": +"elysia@npm:^1.1.26": version: 1.3.5 resolution: "elysia@npm:1.3.5" dependencies: @@ -19676,6 +19407,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.5.0 + resolution: "emoji-regex@npm:10.5.0" + checksum: 10/97537a2cec7c12653bdedf9d87b3c4e2641f12f8f8829765d33959d8e62c6fc23ffe7722ccbdaf3531681725bed0cc201059652f3289fd06925255437a589a49 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -19853,6 +19591,13 @@ __metadata: languageName: node linkType: hard +"environment@npm:^1.0.0": + version: 1.1.0 + resolution: "environment@npm:1.1.0" + checksum: 10/dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6 + languageName: node + linkType: hard + "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" @@ -21161,13 +20906,6 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": - version: 2.1.1 - resolution: "fast-safe-stringify@npm:2.1.1" - checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 - languageName: node - linkType: hard - "fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -21317,18 +21055,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:21.0.0": - version: 21.0.0 - resolution: "file-type@npm:21.0.0" - dependencies: - "@tokenizer/inflate": "npm:^0.2.7" - strtok3: "npm:^10.2.2" - token-types: "npm:^6.0.0" - uint8array-extras: "npm:^1.4.0" - checksum: 10/6980e8b0ef870a98b51ab2eac5db94a1884de8476fe49dc02d2f7e0c1d1d7d44d42b6c59e67867ae90f321ddf4edd00fcfda01821591e2fa05385d0e438a9dc1 - languageName: node - linkType: hard - "file-type@npm:5.2.0, file-type@npm:^5.2.0": version: 5.2.0 resolution: "file-type@npm:5.2.0" @@ -22099,6 +21825,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.3.0 + resolution: "get-east-asian-width@npm:1.3.0" + checksum: 10/8e8e779eb28701db7fdb1c8cab879e39e6ae23f52dadd89c8aed05869671cee611a65d4f8557b83e981428623247d8bc5d0c7a4ef3ea7a41d826e73600112ad8 + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -22905,6 +22638,30 @@ __metadata: languageName: node linkType: hard +"homeserver@workspace:homeserver": + version: 0.0.0-use.local + resolution: "homeserver@workspace:homeserver" + dependencies: + "@biomejs/biome": "npm:^1.9.4" + "@types/bun": "npm:latest" + "@types/express": "npm:^5.0.1" + "@types/node": "npm:^22.15.18" + "@types/sinon": "npm:^17.0.4" + dotenv: "npm:^16.5.0" + husky: "npm:^9.1.7" + lint-staged: "npm:^16.1.2" + pino: "npm:^9.7.0" + pino-pretty: "npm:^13.0.0" + reflect-metadata: "npm:^0.2.2" + sinon: "npm:^20.0.0" + tsconfig-paths: "npm:^4.2.0" + tsyringe: "npm:^4.10.0" + turbo: "npm:~2.5.6" + tweetnacl: "npm:^1.0.3" + typescript: "npm:~5.9.2" + languageName: unknown + linkType: soft + "hono@npm:^3.11.0": version: 3.12.12 resolution: "hono@npm:3.12.12" @@ -24114,6 +23871,22 @@ __metadata: languageName: node linkType: hard +"is-fullwidth-code-point@npm:^4.0.0": + version: 4.0.0 + resolution: "is-fullwidth-code-point@npm:4.0.0" + checksum: 10/8ae89bf5057bdf4f57b346fb6c55e9c3dd2549983d54191d722d5c739397a903012cc41a04ee3403fd872e811243ef91a7c5196da7b5841dc6b6aae31a264a8d + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^5.0.0": + version: 5.0.0 + resolution: "is-fullwidth-code-point@npm:5.0.0" + dependencies: + get-east-asian-width: "npm:^1.0.0" + checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 + languageName: node + linkType: hard + "is-generator-fn@npm:^2.0.0, is-generator-fn@npm:^2.1.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -24713,13 +24486,6 @@ __metadata: languageName: node linkType: hard -"iterare@npm:1.2.1": - version: 1.2.1 - resolution: "iterare@npm:1.2.1" - checksum: 10/ee8322dd9d92e86d8653c899df501c58c5b8e90d6767cf2af0b6d6dc5a4b9b7ed8bce936976f4f4c3a55be110a300c8a7d71967d03f72e104e8db66befcfd874 - languageName: node - linkType: hard - "iterate-iterator@npm:^1.0.1": version: 1.0.2 resolution: "iterate-iterator@npm:1.0.2" @@ -25880,7 +25646,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: @@ -26240,24 +26006,6 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^8.5.1": - version: 8.5.1 - resolution: "jsonwebtoken@npm:8.5.1" - dependencies: - jws: "npm:^3.2.2" - lodash.includes: "npm:^4.3.0" - lodash.isboolean: "npm:^3.0.3" - lodash.isinteger: "npm:^4.0.4" - lodash.isnumber: "npm:^3.0.3" - lodash.isplainobject: "npm:^4.0.6" - lodash.isstring: "npm:^4.0.1" - lodash.once: "npm:^4.0.0" - ms: "npm:^2.1.1" - semver: "npm:^5.6.0" - checksum: 10/a7b52ea570f70bea183ceca970c003f223d9d3425d72498002e9775485c7584bfa3751d1c7291dbb59738074cba288effe73591b87bec5d467622ab3a156fdb6 - languageName: node - linkType: hard - "jsprim@npm:^1.2.2": version: 1.4.2 resolution: "jsprim@npm:1.4.2" @@ -26565,13 +26313,6 @@ __metadata: languageName: node linkType: hard -"libphonenumber-js@npm:^1.11.1": - version: 1.12.9 - resolution: "libphonenumber-js@npm:1.12.9" - checksum: 10/9ec49349ccc68b40fe46e7aca3335e203261d194c780fd297c2f4eade5c9e3745197efccdafe9bc9ec5d650663c94bd464c6c256f3a2fa3f53f140c54470a6e5 - languageName: node - linkType: hard - "libqp@npm:2.1.1": version: 2.1.1 resolution: "libqp@npm:2.1.1" @@ -26645,10 +26386,37 @@ __metadata: languageName: node linkType: hard -"load-esm@npm:1.0.2": - version: 1.0.2 - resolution: "load-esm@npm:1.0.2" - checksum: 10/1b4adb40c28c6fdbd4ca8c97942c04debddb3c93ae91413540ff5a21ca3511a651988c835cb80cad7288d1ecb869c4794b8a787ab02e09cc07ec951ad1eefcf9 +"lint-staged@npm:^16.1.2": + version: 16.1.2 + resolution: "lint-staged@npm:16.1.2" + dependencies: + chalk: "npm:^5.4.1" + commander: "npm:^14.0.0" + debug: "npm:^4.4.1" + lilconfig: "npm:^3.1.3" + listr2: "npm:^8.3.3" + micromatch: "npm:^4.0.8" + nano-spawn: "npm:^1.0.2" + pidtree: "npm:^0.6.0" + string-argv: "npm:^0.3.2" + yaml: "npm:^2.8.0" + bin: + lint-staged: bin/lint-staged.js + checksum: 10/90df77c2f59cdc5ebeb8a60767f07025a8aed9161f604fea6cf1ca895ff3b56995a00145a3e0b5c0bf22e8f667a6182256b68e001e5f3118e46a3c5150bede82 + languageName: node + linkType: hard + +"listr2@npm:^8.3.3": + version: 8.3.3 + resolution: "listr2@npm:8.3.3" + dependencies: + cli-truncate: "npm:^4.0.0" + colorette: "npm:^2.0.20" + eventemitter3: "npm:^5.0.1" + log-update: "npm:^6.1.0" + rfdc: "npm:^1.4.1" + wrap-ansi: "npm:^9.0.0" + checksum: 10/92f1bb60e9a0f4fed9bff89fbab49d80fc889d29cf47c0a612f5a62a036dead49d3f697d3a79e36984768529bd3bfacb3343859eafceba179a8e66c034d99300 languageName: node linkType: hard @@ -26892,6 +26660,19 @@ __metadata: languageName: node linkType: hard +"log-update@npm:^6.1.0": + version: 6.1.0 + resolution: "log-update@npm:6.1.0" + dependencies: + ansi-escapes: "npm:^7.0.0" + cli-cursor: "npm:^5.0.0" + slice-ansi: "npm:^7.1.0" + strip-ansi: "npm:^7.1.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10/5abb4131e33b1e7f8416bb194fe17a3603d83e4657c5bf5bb81ce4187f3b00ea481643b85c3d5cefe6037a452cdcf7f1391ab8ea0d9c23e75d19589830ec4f11 + languageName: node + linkType: hard + "logform@npm:^2.6.0, logform@npm:^2.6.1": version: 2.6.1 resolution: "logform@npm:2.6.1" @@ -27709,6 +27490,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + "mimic-response@npm:^1.0.0": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -28342,6 +28130,13 @@ __metadata: languageName: node linkType: hard +"nano-spawn@npm:^1.0.2": + version: 1.0.2 + resolution: "nano-spawn@npm:1.0.2" + checksum: 10/6ce9e60846d2e37c0e3cd048472683c81dbcaadef9ebe73bfc8754ee7da2a574f724436d3dcdeda5d807aedc857cc8cbc278a9882529164b5ef4b170b95cfe0b + languageName: node + linkType: hard + "nanoid@npm:3.3.1": version: 3.3.1 resolution: "nanoid@npm:3.3.1" @@ -28360,30 +28155,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" - bin: - nanoid: bin/nanoid.cjs - checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 - languageName: node - linkType: hard - "nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" - bin: - nanoid: bin/nanoid.cjs - checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 - languageName: node - linkType: hard - -"nanoid@npm:^3.3.8": version: 3.3.8 resolution: "nanoid@npm:3.3.8" bin: nanoid: bin/nanoid.js - checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 + checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 languageName: node linkType: hard @@ -28396,13 +28173,6 @@ __metadata: languageName: node linkType: hard -"napi-build-utils@npm:^1.0.1": - version: 1.0.2 - resolution: "napi-build-utils@npm:1.0.2" - checksum: 10/276feb8e30189fe18718e85b6f82e4f952822baa2e7696f771cc42571a235b789dc5907a14d9ffb6838c3e4ff4c25717c2575e5ce1cf6e02e496e204c11e57f6 - languageName: node - linkType: hard - "napi-build-utils@npm:^2.0.0": version: 2.0.0 resolution: "napi-build-utils@npm:2.0.0" @@ -28428,15 +28198,6 @@ __metadata: languageName: node linkType: hard -"nats@npm:^2.29.3": - version: 2.29.3 - resolution: "nats@npm:2.29.3" - dependencies: - nkeys.js: "npm:1.1.0" - checksum: 10/c60b0c61a23afa3412e16c95a486a19c1a48544a9de4eb7004eaddba06f2f341f6aa475924158045b851cba6755ed4d46832b894b45d9c7342df674aa6e2b7ff - languageName: node - linkType: hard - "natural-compare-lite@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare-lite@npm:1.4.0" @@ -28650,15 +28411,6 @@ __metadata: languageName: node linkType: hard -"node-jsonwebtoken@npm:^0.0.1": - version: 0.0.1 - resolution: "node-jsonwebtoken@npm:0.0.1" - dependencies: - jsonwebtoken: "npm:^8.5.1" - checksum: 10/d3b85a996409e2900bbb1c6cf0ee02e4df1f3344ac722f31bad0f5ed0569dfe0d14a8be0189dee46e96096a97719b1bf57dacee44284b390153ffd7fbb1fba62 - languageName: node - linkType: hard - "node-noop@npm:^0.0.1": version: 0.0.1 resolution: "node-noop@npm:0.0.1" @@ -29176,6 +28928,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: "npm:^5.0.0" + checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + "only@npm:^0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -29927,13 +29688,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:8.2.0, path-to-regexp@npm:^8.1.0": - version: 8.2.0 - resolution: "path-to-regexp@npm:8.2.0" - checksum: 10/23378276a172b8ba5f5fb824475d1818ca5ccee7bbdb4674701616470f23a14e536c1db11da9c9e6d82b82c556a817bbf4eee6e41b9ed20090ef9427cbb38e13 - languageName: node - linkType: hard - "path-to-regexp@npm:^6.3.0": version: 6.3.0 resolution: "path-to-regexp@npm:6.3.0" @@ -30120,6 +29874,15 @@ __metadata: languageName: node linkType: hard +"pidtree@npm:^0.6.0": + version: 0.6.0 + resolution: "pidtree@npm:0.6.0" + bin: + pidtree: bin/pidtree.js + checksum: 10/ea67fb3159e170fd069020e0108ba7712df9f0fd13c8db9b2286762856ddce414fb33932e08df4bfe36e91fe860b51852aee49a6f56eb4714b69634343add5df + languageName: node + linkType: hard + "pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -30239,7 +30002,14 @@ __metadata: languageName: node linkType: hard -"pino@npm:8.21.0": +"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, pino@npm:^8.16.0, pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" dependencies: @@ -30260,24 +30030,24 @@ __metadata: languageName: node linkType: hard -"pino@npm:^8.21.0": - version: 8.21.0 - resolution: "pino@npm:8.21.0" +"pino@npm:^9.6.0, pino@npm:^9.7.0": + version: 9.10.0 + resolution: "pino@npm:9.10.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:^1.2.0" - pino-std-serializers: "npm:^6.0.0" - process-warning: "npm:^3.0.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:^3.7.0" - thread-stream: "npm:^2.6.0" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" bin: pino: bin.js - checksum: 10/5a054eab533ab91b20f63497b86070f0a6b40e4688cde9de66d23e03d6046c4e95d69c3f526dea9f30bcbc5874c7fbf0f91660cded4753946fd02261ca8ac340 + checksum: 10/02962e12ae7692c763da6f64c2037f113a03f5f7e6e6be96f0878c4aa4095ca8cf8ffbac561eda1ac367c7ea1f7ee50d44d211674f314b2560ddd3db1e4efb5f languageName: node linkType: hard @@ -31329,6 +31099,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" @@ -31377,13 +31154,6 @@ __metadata: languageName: node linkType: hard -"promise-breaker@npm:^6.0.0": - version: 6.0.0 - resolution: "promise-breaker@npm:6.0.0" - checksum: 10/6f7ad5e55d3f434dc1e02907c3294dc4a44f9962d9af9de186095c75c8f76d11feeb927e96ec9e177fcc9209690defb5c64eeac4767e7c3dd4f120e9d14fb0c8 - languageName: node - linkType: hard - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -33002,6 +32772,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: "npm:^7.0.0" + signal-exit: "npm:^4.1.0" + checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c + languageName: node + linkType: hard + "restructure@npm:^3.0.0": version: 3.0.0 resolution: "restructure@npm:3.0.0" @@ -33055,6 +32835,13 @@ __metadata: languageName: node linkType: hard +"rfdc@npm:^1.4.1": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729 + languageName: node + linkType: hard + "rimraf@npm:^2.5.4": version: 2.7.1 resolution: "rimraf@npm:2.7.1" @@ -33293,15 +33080,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -33624,15 +33402,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.2": - version: 7.7.2 - resolution: "semver@npm:7.7.2" - bin: - semver: bin/semver.js - checksum: 10/7a24cffcaa13f53c09ce55e05efe25cd41328730b2308678624f8b9f5fc3093fc4d189f47950f0b811ff8f3c3039c24a2c36717ba7961615c682045bf03e1dda - languageName: node - linkType: hard - "semver@npm:~5.3.0": version: 5.3.0 resolution: "semver@npm:5.3.0" @@ -33997,7 +33766,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -34125,6 +33894,26 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^5.0.0": + version: 5.0.0 + resolution: "slice-ansi@npm:5.0.0" + dependencies: + ansi-styles: "npm:^6.0.0" + is-fullwidth-code-point: "npm:^4.0.0" + checksum: 10/7e600a2a55e333a21ef5214b987c8358fe28bfb03c2867ff2cbf919d62143d1812ac27b4297a077fdaf27a03da3678e49551c93e35f9498a3d90221908a1180e + languageName: node + linkType: hard + +"slice-ansi@npm:^7.1.0": + version: 7.1.0 + resolution: "slice-ansi@npm:7.1.0" + dependencies: + ansi-styles: "npm:^6.2.1" + is-fullwidth-code-point: "npm:^5.0.0" + checksum: 10/10313dd3cf7a2e4b265f527b1684c7c568210b09743fd1bd74f2194715ed13ffba653dc93a5fa79e3b1711518b8990a732cb7143aa01ddafe626e99dfa6474b2 + languageName: node + linkType: hard + "slick@npm:^1.12.2": version: 1.12.2 resolution: "slick@npm:1.12.2" @@ -34782,6 +34571,13 @@ __metadata: languageName: node linkType: hard +"string-argv@npm:^0.3.2": + version: 0.3.2 + resolution: "string-argv@npm:0.3.2" + checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af + languageName: node + linkType: hard + "string-length@npm:^4.0.1, string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -34838,6 +34634,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/42f9e82f61314904a81393f6ef75b832c39f39761797250de68c041d8ba4df2ef80db49ab6cd3a292923a6f0f409b8c9980d120f7d32c820b4a8a84a2598a295 + languageName: node + linkType: hard + "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -34972,7 +34779,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -35066,15 +34873,6 @@ __metadata: languageName: node linkType: hard -"strtok3@npm:^10.2.2": - version: 10.3.1 - resolution: "strtok3@npm:10.3.1" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - checksum: 10/bb7950cc9ce98ec742a5db360630f0b004f16197959ae28d8c8dad4f8f0e405d71cfdc992483038ba29a0b4cbd7227618ad2492005b510d84a3fc5903df0c13f - languageName: node - linkType: hard - "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -35754,6 +35552,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" @@ -35920,6 +35727,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" @@ -35962,16 +35776,6 @@ __metadata: languageName: node linkType: hard -"token-types@npm:^6.0.0": - version: 6.0.0 - resolution: "token-types@npm:6.0.0" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - ieee754: "npm:^1.2.1" - checksum: 10/b541b605d602e8e6495745badb35f90ee8f997e43dc29bc51aee7e9a0bc3c6bc7372a305bd45f3e80d75223c2b6a5c7e65cb5159d8c4e49fa25cdbaae531fad4 - languageName: node - linkType: hard - "tough-cookie@npm:^2.3.3, tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -36236,23 +36040,6 @@ __metadata: languageName: node linkType: hard -"ts-patch@npm:^3.3.0": - version: 3.3.0 - resolution: "ts-patch@npm:3.3.0" - dependencies: - chalk: "npm:^4.1.2" - global-prefix: "npm:^4.0.0" - minimist: "npm:^1.2.8" - resolve: "npm:^1.22.2" - semver: "npm:^7.6.3" - strip-ansi: "npm:^6.0.1" - bin: - ts-patch: bin/ts-patch.js - tspc: bin/tspc.js - checksum: 10/5b0a42cacdfef2136b18b785b4ca6579d470001560d42139057fccf6ed85a622a73e5efba1b20426b047bf9aaf2090a23a9afca4e72ab330b166344dd45b3386 - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.15.0": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0" @@ -36283,20 +36070,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 - languageName: node - linkType: hard - -"tslib@npm:^1.8.1": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb - languageName: node - linkType: hard - "tslib@npm:^1.8.1, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -36439,13 +36212,6 @@ __metadata: languageName: node linkType: hard -"tweetnacl@npm:1.0.3": - version: 1.0.3 - resolution: "tweetnacl@npm:1.0.3" - checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 - languageName: node - linkType: hard - "tweetnacl@npm:1.0.3, tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3" @@ -36558,13 +36324,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.41.0": - version: 4.41.0 - resolution: "type-fest@npm:4.41.0" - checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 - languageName: node - linkType: hard - "type-is@npm:1.6.18, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -36661,7 +36420,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.0, typescript@npm:^5.4.2, typescript@npm:^5.8.3, 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: @@ -36671,17 +36430,17 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.9.2": - version: 5.9.2 - resolution: "typescript@npm:5.9.2" +"typescript@npm:~5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/cc2fe6c822819de5d453fa25aa9f32096bf70dde215d481faa1ad84a283dfb264e33988ed8f6d36bc803dd0b16dbe943efa311a798ef76d5b3892a05dfbfd628 + checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.3.0#optional!builtin, typescript@patch:typescript@npm%3A^5.4.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin, typescript@patch:typescript@npm%3A~5.9.2#optional!builtin": +"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: @@ -36691,48 +36450,13 @@ __metadata: languageName: node linkType: hard -"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" +"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/bd810ab13e8e557225a8b5122370385440b933e4e077d5c7641a8afd207fdc8be9c346e3c678adba934b64e0e70b0acf5eef9493ea05170a48ce22bef845fdc7 - languageName: node - linkType: hard - -"typia@npm:^5.5.7": - version: 5.5.10 - resolution: "typia@npm:5.5.10" - dependencies: - commander: "npm:^10.0.0" - comment-json: "npm:^4.2.3" - inquirer: "npm:^8.2.5" - randexp: "npm:^0.5.3" - peerDependencies: - typescript: ">=4.8.0 <5.5.0" - bin: - typia: lib/executable/typia.js - checksum: 10/d3dd44474ebd927f1aa1744812676db20fccf475d0808dae05cad8a65c60a0c4ca83cd3073dccbe32412e6eaa319c20715f775042ae42fc64d950ec1f13bc6f3 - languageName: node - linkType: hard - -"typia@npm:~9.3.1": - version: 9.3.1 - resolution: "typia@npm:9.3.1" - dependencies: - "@samchon/openapi": "npm:^4.3.1" - "@standard-schema/spec": "npm:^1.0.0" - commander: "npm:^10.0.0" - comment-json: "npm:^4.2.3" - inquirer: "npm:^8.2.5" - package-manager-detector: "npm:^0.2.0" - randexp: "npm:^0.5.3" - peerDependencies: - typescript: ">=4.8.0 <5.10.0" - bin: - typia: lib/executable/typia.js - checksum: 10/1f0b260402900b46225af7bba1110399a21a9276122a306b34448a4d6f9de12035857f520baf1ee27a663274835a85e6f9cdeb856c866e437b544b87164d48f9 + checksum: 10/b9b1e73dabac5dc730c041325dbd9c99467c1b0d239f1b74ec3b90d831384af3e2ba973946232df670519147eb51a2c20f6f96163cea2b359f03de1e2091cc4f languageName: node linkType: hard @@ -36783,22 +36507,6 @@ __metadata: languageName: node linkType: hard -"uid@npm:2.0.2": - version: 2.0.2 - resolution: "uid@npm:2.0.2" - dependencies: - "@lukeed/csprng": "npm:^1.0.0" - checksum: 10/18f6da43d8e1b8643077e8123f877b4506759d9accc15337140a1bf7c99f299a66e88b27ab4c640e66e6a10f19e3a85afa45fdf830dd4bab7570d07a3d51e073 - languageName: node - linkType: hard - -"uint8array-extras@npm:^1.4.0": - version: 1.4.0 - resolution: "uint8array-extras@npm:1.4.0" - checksum: 10/4d2955d67c112e5ebaa4901272a75fc9ad14902c40f05a178b01e32387aa2702b6840472d931a1ca16e068ac59013c7d9ee2b4b2f141c4e73ba4bc7456490599 - languageName: node - linkType: hard - "umd@npm:^3.0.0": version: 3.0.3 resolution: "umd@npm:3.0.3" @@ -37215,16 +36923,6 @@ __metadata: languageName: node linkType: hard -"url-parse@npm:^1.5.10, url-parse@npm:~1.5.10": - version: 1.5.10 - resolution: "url-parse@npm:1.5.10" - dependencies: - querystringify: "npm:^2.1.1" - requires-port: "npm:^1.0.0" - checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad - languageName: node - linkType: hard - "url-to-options@npm:^1.0.1": version: 1.0.1 resolution: "url-to-options@npm:1.0.1" @@ -37334,13 +37032,6 @@ __metadata: languageName: node linkType: hard -"utils-merge@npm:1.0.1": - version: 1.0.1 - resolution: "utils-merge@npm:1.0.1" - checksum: 10/5d6949693d58cb2e636a84f3ee1c6e7b2f9c16cb1d42d0ecb386d8c025c69e327205aa1c69e2868cc06a01e5e20681fbba55a4e0ed0cce913d60334024eae798 - languageName: node - linkType: hard - "utils-merge@npm:1.0.1, utils-merge@npm:^1.0.0": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" @@ -37435,13 +37126,6 @@ __metadata: languageName: node linkType: hard -"validator@npm:^13.9.0": - version: 13.15.15 - resolution: "validator@npm:13.15.15" - checksum: 10/a43d9271c879468b1ad6dd5d2597b71719a185d2c7ceb3d68f3c9c8c17c25af0d90a53edfa7efaa6ac0d4425ba0345684b9c7d8111bde0d06d915a634a287018 - languageName: node - linkType: hard - "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -38188,6 +37872,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/b9d91564c091cf3978a7c18ca0f3e4d4606e83549dbe59cf76f5e77feefdd5ec91443155e8102630524d10a8c275efac8a7082c0f26fa43e6b989dc150d176ce + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -38582,16 +38277,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.24.5": - version: 3.24.5 - resolution: "zod-to-json-schema@npm:3.24.5" - peerDependencies: - zod: ^3.24.1 - checksum: 10/1af291b4c429945c9568c2e924bdb7c66ab8d139cbeb9a99b6e9fc9e1b02863f85d07759b9303714f07ceda3993dcaf0ebcb80d2c18bb2aaf5502b2c1016affd - languageName: node - linkType: hard - -"zod@npm:^3.22.0, zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.3": +"zod@npm:^3.22.0, zod@npm:^3.22.4, zod@npm:^3.23.8": version: 3.25.67 resolution: "zod@npm:3.25.67" checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa From f85ef2721958c30123bd79f24c922066d2c5413c Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 9 Jul 2025 09:56:09 -0300 Subject: [PATCH 11/99] chore: add federation routes to RC (monolith + service) (#36384) --- apps/meteor/app/api/server/api.ts | 9 - apps/meteor/ee/server/api/federation.ts | 125 +---- ee/apps/federation-service/src/service.ts | 31 +- ee/packages/federation-matrix/package.json | 4 +- .../federation-matrix/src/FederationMatrix.ts | 3 +- .../src/api/.well-known/server.ts | 43 ++ .../src/api/_matrix/invite.ts | 156 ++++++ .../src/api/_matrix/key/server.ts | 57 +++ .../src/api/_matrix/profiles.ts | 484 ++++++++++++++++++ .../src/api/_matrix/rooms.ts | 207 ++++++++ .../src/api/_matrix/send-join.ts | 245 +++++++++ .../src/api/_matrix/transactions.ts | 199 +++++++ .../src/api/_matrix/versions.ts | 56 ++ ee/packages/federation-matrix/src/api/api.ts | 31 ++ .../federation-matrix/src/events/invite.ts | 3 +- .../federation-matrix/src/events/ping.ts | 2 +- .../federation-matrix/src/setupContainers.ts | 9 +- packages/core-services/package.json | 1 + .../src/types/IFederationMatrixService.ts | 8 +- packages/http-router/src/Router.ts | 8 + yarn.lock | 3 +- 21 files changed, 1513 insertions(+), 171 deletions(-) create mode 100644 ee/packages/federation-matrix/src/api/.well-known/server.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/invite.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/key/server.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/profiles.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/rooms.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/send-join.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/transactions.ts create mode 100644 ee/packages/federation-matrix/src/api/_matrix/versions.ts create mode 100644 ee/packages/federation-matrix/src/api/api.ts diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 7138e187d404d..74f3ee477bcb8 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -11,7 +11,6 @@ import { metricsMiddleware } from './middlewares/metrics'; import { remoteAddressMiddleware } from './middlewares/remoteAddressMiddleware'; import { tracerSpanMiddleware } from './middlewares/tracer'; import { type APIActionHandler, RocketChatAPIRouter } from './router'; -import { isRunningMs } from '../../../server/lib/isRunningMs'; import { metrics } from '../../metrics/server'; import { settings } from '../../settings/server'; @@ -107,14 +106,6 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => }); export const startRestAPI = () => { - // Register federation routes at root level if enabled and not running in MS mode - if (settings.get('Federation_Service_Enabled') && !isRunningMs()) { - (WebApp.rawConnectHandlers as unknown as ReturnType) - .use(API._matrix.router) - .use(API.wellKnown.router) - .use(API.matrixInternal.router); - } - // Register main API routes under /api prefix (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api diff --git a/apps/meteor/ee/server/api/federation.ts b/apps/meteor/ee/server/api/federation.ts index 8a9ca6803669e..278282ed0ec1b 100644 --- a/apps/meteor/ee/server/api/federation.ts +++ b/apps/meteor/ee/server/api/federation.ts @@ -1,18 +1,10 @@ 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 { API } from '../../../app/api/server'; import { isRunningMs } from '../../../server/lib/isRunningMs'; -interface IExtendedContext { - urlParams?: Record; - queryParams?: Record; - bodyParams?: Record; - request?: Request; - _statusCode?: number; - _headers?: Record; -} - const logger = new Logger('FederationRoutes'); export async function registerFederationRoutes(federationService: IFederationMatrixService): Promise { @@ -22,118 +14,9 @@ export async function registerFederationRoutes(federationService: IFederationMat try { const routes = federationService.getAllRoutes(); + (WebApp.rawConnectHandlers as unknown as ReturnType).use(routes.matrix.router).use(routes.wellKnown.router); - for (const route of routes) { - const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'; - - let router: any; - if (route.path.startsWith('/_matrix')) { - router = API._matrix; - } else if (route.path.startsWith('/.well-known')) { - router = API.wellKnown; - } else if (route.path.startsWith('/internal')) { - router = API.matrixInternal; - } else { - logger.error(`Unknown route prefix for path: ${route.path}`); - continue; - } - - if (method === 'patch') { - if (typeof (router as any).method === 'function') { - const routePath = route.path.replace(/^\/_matrix|^\/\.well-known|^\/internal/, ''); - (router as any).method('PATCH', routePath || '/', { response: {} }, async function (this: IExtendedContext) { - try { - const context = { - params: this.urlParams || {}, - query: this.queryParams || {}, - body: this.bodyParams || {}, - headers: this.request?.headers ? Object.fromEntries(this.request.headers.entries()) : {}, - setStatus: (code: number) => { - this._statusCode = code; - }, - setHeader: (key: string, value: string) => { - if (!this._headers) { - this._headers = {}; - } - this._headers[key] = value; - }, - }; - - const response = await route.handler(context); - - const result: any = { - statusCode: this._statusCode || 200, - body: response, - }; - - if (this._headers) { - result.headers = this._headers; - } - - return result; - } catch (error) { - logger.error(`Error handling route: ${route.path}`, error); - return { - statusCode: 500, - body: { error: 'Internal server error' }, - }; - } - }); - continue; - } else { - logger.error(`Cannot register PATCH method for route ${route.path} - method() function not available`); - continue; - } - } - - if (typeof (router as any)[method] !== 'function') { - logger.error(`Method ${method} not found on router for path: ${route.path}`); - continue; - } - - const routePath = route.path.replace(/^\/_matrix|^\/\.well-known|^\/internal/, ''); - - (router as any)[method](routePath || '/', { response: {} }, async function (this: IExtendedContext) { - try { - const context = { - params: this.urlParams || {}, - query: this.queryParams || {}, - body: this.bodyParams || {}, - headers: this.request?.headers ? Object.fromEntries(this.request.headers.entries()) : {}, - setStatus: (code: number) => { - this._statusCode = code; - }, - setHeader: (key: string, value: string) => { - if (!this._headers) { - this._headers = {}; - } - this._headers[key] = value; - }, - }; - - const response = await route.handler(context); - - const result: any = { - statusCode: this._statusCode || 200, - body: response, - }; - - if (this._headers) { - result.headers = this._headers; - } - - return result; - } catch (error) { - logger.error(`Error handling route: ${route.path}`, error); - return { - statusCode: 500, - body: { error: 'Internal server error' }, - }; - } - }); - } - - logger.log('[Federation] Registered', routes.length, 'federation routes'); + logger.log('[Federation] Registered federation routes'); } catch (error) { logger.error('[Federation] Failed to register routes:', error); throw error; diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 180dcd4cf464c..46e69b868dff9 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -1,37 +1,12 @@ import 'reflect-metadata'; import { serve } from '@hono/node-server'; import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; -// import type { RouteDefinition, RouteContext } from '@hs/federation-sdk'; import { registerServiceModels } from '@rocket.chat/models'; import { startBroker } from '@rocket.chat/network-broker'; import { Hono } from 'hono'; import { config } from './config'; -// export function handleFederationRoutesRegistration(app: Hono, homeserverRoutes: RouteDefinition[]): Hono { -// // console.info(`Registering ${homeserverRoutes.length} homeserver routes`); -// // for (const route of homeserverRoutes) { -// // const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'; -// // app[method](route.path, async (c) => { -// // try { -// // const context = { -// // req: c.req, -// // res: c.res, -// // params: c.req.param(), -// // query: c.req.query(), -// // body: await c.req.json().catch(() => ({})), -// // }; -// // const result = await route.handler(context as unknown as RouteContext); -// // return c.json(result); -// // } catch (error) { -// // console.error(`Error handling route ${method.toUpperCase()} ${route.path}:`, error); -// // return c.json({ error: 'Internal server error' }, 500); -// // } -// // }); -// // } -// // return app; -// } - function handleHealthCheck(app: Hono) { app.get('/health', async (c) => { try { @@ -56,7 +31,11 @@ function handleHealthCheck(app: Hono) { api.registerService(federationMatrix); const app = new Hono(); - // handleFederationRoutesRegistration(app, federationMatrix.getAllRoutes()); + const { matrix, wellKnown } = federationMatrix.getAllRoutes(); + + app.mount('/_matrix', matrix.getHonoRouter().fetch); + app.mount('/.well-known', wellKnown.getHonoRouter().fetch); + handleHealthCheck(app); serve({ diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index f7be2f5e6c2ee..89327fe46a086 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -7,9 +7,7 @@ "@babel/core": "~7.26.0", "@babel/preset-env": "~7.26.0", "@babel/preset-typescript": "~7.26.0", - "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/rest-typings": "workspace:^", "@types/node": "~22.14.0", "babel-jest": "~30.0.0", "eslint": "~8.45.0", @@ -38,8 +36,10 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/http-router": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/network-broker": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", "mongodb": "6.10.0", "pino": "8.21.0", "reflect-metadata": "^0.2.2" diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 7aacaf32ac30b..17ebf9fece781 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -8,6 +8,7 @@ import { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; +import { getAllMatrixRoutes } from './api/api'; import { registerEvents } from './events'; import { setup } from './setupContainers'; @@ -62,7 +63,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } getAllRoutes() { - return []; + return getAllMatrixRoutes(); } async createRoom(room: IRoom, owner: IUser, members: string[]): Promise { 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..1deef06292e87 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -0,0 +1,43 @@ +import type { Router } from "@rocket.chat/http-router"; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; +import { getAllServicesFromFederationSDK } from '../../setupContainers'; +import { createHash } from 'node:crypto'; + +const WellKnownServerResponseSchema = { + type: 'object', + properties: { + 'm.server': { + type: 'string', + description: 'Matrix server address with port' + } + }, + required: ['m.server'] +}; + +const isWellKnownServerResponseProps = ajv.compile(WellKnownServerResponseSchema); + +export const getWellKnownRoutes = (router: Router<'/.well-known'>) => { + const { wellKnown } = getAllServicesFromFederationSDK(); + + return router.get('/matrix/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..df4438bebd91a --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -0,0 +1,156 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../setupContainers'; + +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 isProcessInviteBodyProps = ajv.compile(RoomMemberEventSchema); + +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); + +export const getMatrixInviteRoutes = (router: Router<'/_matrix'>) => { + const { invite } = getAllServicesFromFederationSDK(); + + return router.put( + '/federation/v2/invite/:roomId/:eventId', + { + body: isProcessInviteBodyProps, + params: isProcessInviteParamsProps, + response: { + 200: isProcessInviteResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { roomId, eventId } = c.req.param(); + const body = await c.req.json(); + + const response = await invite.processInvite(body, roomId, eventId); + + return { + body: response, + 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..af5c76de2e951 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts @@ -0,0 +1,57 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../../setupContainers'; + +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 = (router: Router<'/_matrix'>) => { + const { server } = getAllServicesFromFederationSDK(); + + return router.get( + '/key/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/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts new file mode 100644 index 0000000000000..400d412c59185 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -0,0 +1,484 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../setupContainers'; + +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, + }, + 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'], +}; + +const isMakeJoinParamsProps = ajv.compile(MakeJoinParamsSchema); + +const MakeJoinQuerySchema = { + type: 'object', + properties: { + ver: { + type: 'array', + items: { + type: 'string', + }, + minItems: 0, + description: 'Supported room versions', + }, + }, +}; + +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'], +}; + +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 = (router: Router<'/_matrix'>) => { + const { profile } = getAllServicesFromFederationSDK(); + + return router + .get( + '/federation/v1/query/profile', + { + query: isQueryProfileQueryProps, + response: { + 200: isQueryProfileResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const { user_id: userId } = c.req.query(); + + const response = await profile.queryProfile(userId); + + return { + body: response, + statusCode: 200, + }; + }, + ) + .post( + '/federation/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( + '/federation/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( + '/federation/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 : ['1']); + + return { + body: { + room_version: response.room_version, + event: { + ...response.event, + content: { + ...response.event.content, + membership: 'join', + join_authorised_via_users_server: response.event.content.join_authorised_via_users_server, + }, + room_id: response.event.room_id, + sender: response.event.sender, + state_key: response.event.state_key, + type: 'm.room.member', + origin_server_ts: response.event.origin_server_ts, + origin: response.event.origin, + }, + }, + statusCode: 200, + }; + }, + ) + .post( + '/federation/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( + '/federation/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..6670a08108d76 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts @@ -0,0 +1,207 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../setupContainers'; + +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 = (router: Router<'/_matrix'>) => { + const { state } = getAllServicesFromFederationSDK(); + + return router + .get( + '/federation/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( + '/federation/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()); + } + + if (filter.room_types) { + // TODO: implement room_types filtering + } + + 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..9c5d38f77ccb4 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -0,0 +1,245 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../setupContainers'; + +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'], +}; + +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'], + }, + ], +}; + +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'], +}; + +const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema); + +export const getMatrixSendJoinRoutes = (router: Router<'/_matrix'>) => { + const { sendJoin } = getAllServicesFromFederationSDK(); + + return router.put( + '/federation/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, 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..93d06bcf6ccf0 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -0,0 +1,199 @@ +import type { Router } from '@rocket.chat/http-router'; +import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; + +import { getAllServicesFromFederationSDK } from '../../setupContainers'; + +const SendTransactionParamsSchema = { + type: 'object', + properties: { + txnId: { + type: 'string', + description: 'Transaction ID', + }, + }, + required: ['txnId'], +}; + +const isSendTransactionParamsProps = ajv.compile(SendTransactionParamsSchema); + +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', 'origin'], +}; + +const SendTransactionBodySchema = { + type: 'object', + properties: { + 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: ['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); + +export const getMatrixTransactionsRoutes = (router: Router<'/_matrix'>) => { + const { event } = getAllServicesFromFederationSDK(); + + return router.put( + '/federation/v1/send/:txnId', + { + params: isSendTransactionParamsProps, + body: isSendTransactionBodyProps, + response: { + 200: isSendTransactionResponseProps, + 400: isErrorResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const body = await c.req.json(); + + const { pdus = [] } = body; + + if (pdus.length === 0) { + return { + body: { + pdus: {}, + edus: {}, + }, + statusCode: 200, + }; + } + + await event.processIncomingPDUs(pdus); + + return { + body: { + pdus: {}, + edus: {}, + }, + 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..cb039856352bb --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -0,0 +1,56 @@ +import { ConfigService } from '@hs/federation-sdk'; +import type { 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 = (router: Router<'/_matrix'>) => { + const configService = new ConfigService(); + + return router.get( + '/federation/v1/version', + { + response: { + 200: isGetVersionsResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async () => { + const config = configService.getServerConfig(); + + const response = { + server: { + name: config.name, + version: config.version, + }, + }; + + return { + body: response, + statusCode: 200, + }; + }, + ); +}; diff --git a/ee/packages/federation-matrix/src/api/api.ts b/ee/packages/federation-matrix/src/api/api.ts new file mode 100644 index 0000000000000..e61a602a8a4c1 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/api.ts @@ -0,0 +1,31 @@ +import { Router } from '@rocket.chat/http-router'; + +import { getWellKnownRoutes } from './.well-known/server'; +import { getMatrixInviteRoutes } from './_matrix/invite'; +import { getKeyServerRoutes } from './_matrix/key/server'; +import { getMatrixProfilesRoutes } from './_matrix/profiles'; +import { getMatrixRoomsRoutes } from './_matrix/rooms'; +import { getMatrixSendJoinRoutes } from './_matrix/send-join'; +import { getMatrixTransactionsRoutes } from './_matrix/transactions'; +import { getFederationVersionsRoutes } from './_matrix/versions'; + +const matrix = new Router('/_matrix'); +const wellKnown = new Router('/.well-known'); + +export const getAllMatrixRoutes = () => { + matrix + .use(getMatrixInviteRoutes(matrix)) + .use(getMatrixProfilesRoutes(matrix)) + .use(getMatrixRoomsRoutes(matrix)) + .use(getMatrixSendJoinRoutes(matrix)) + .use(getMatrixTransactionsRoutes(matrix)) + .use(getFederationVersionsRoutes(matrix)) + .use(getKeyServerRoutes(matrix)); + + wellKnown.use(getWellKnownRoutes(wellKnown)); + + return { + matrix, + wellKnown, + }; +}; diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index 38281bc60d1c5..e1592be26bc4d 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -1,8 +1,7 @@ - +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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 '@hs/federation-sdk'; import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; export function invite(emitter: Emitter) { diff --git a/ee/packages/federation-matrix/src/events/ping.ts b/ee/packages/federation-matrix/src/events/ping.ts index d2d116374cb8a..04972b23fb544 100644 --- a/ee/packages/federation-matrix/src/events/ping.ts +++ b/ee/packages/federation-matrix/src/events/ping.ts @@ -1,5 +1,5 @@ -import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import type { Emitter } from '@rocket.chat/emitter'; export const ping = async (emitter: Emitter) => { emitter.on('homeserver.ping', async (data) => { diff --git a/ee/packages/federation-matrix/src/setupContainers.ts b/ee/packages/federation-matrix/src/setupContainers.ts index 7e24cbe92c989..290f3da9d2db1 100644 --- a/ee/packages/federation-matrix/src/setupContainers.ts +++ b/ee/packages/federation-matrix/src/setupContainers.ts @@ -1,8 +1,8 @@ import 'reflect-metadata'; import { toUnpaddedBase64 } from '@hs/core'; -import { ConfigService, createFederationContainer } from '@hs/federation-sdk'; -import type { DependencyContainer, FederationContainerOptions, HomeserverEventSignatures } from '@hs/federation-sdk'; +import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; +import type { DependencyContainer, FederationContainerOptions, HomeserverEventSignatures, HomeserverServices } from '@hs/federation-sdk'; import { Emitter } from '@rocket.chat/emitter'; let container: DependencyContainer | undefined; @@ -32,9 +32,10 @@ export async function setup( return container; } -export function getContainer(): DependencyContainer { +export function getAllServicesFromFederationSDK(): HomeserverServices { if (!container) { throw new Error('Federation container is not initialized. Call setup() first.'); } - return container; + + return getAllServices(container); } 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/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 54d2920c41d51..02bd33f95eebf 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,4 +1,5 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { Router } from '@rocket.chat/http-router'; export interface IRouteContext { params: any; @@ -11,10 +12,9 @@ export interface IRouteContext { export interface IFederationMatrixService { getAllRoutes(): { - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - path: string; - handler: (ctx: IRouteContext) => Promise; - }[]; + matrix: Router<'/_matrix'>; + wellKnown: Router<'/.well-known'>; + }; createRoom(room: IRoom, owner: IUser, members: string[]): Promise; sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; } 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/yarn.lock b/yarn.lock index 6be2e48aa64d0..de2637f1f6f7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7516,6 +7516,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:^" @@ -7722,11 +7723,11 @@ __metadata: "@babel/preset-env": "npm:~7.26.0" "@babel/preset-typescript": "npm:~7.26.0" "@hs/federation-sdk": "workspace:^" - "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/http-router": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" From 5c14cb76d9df089f8a0f8f853493fc36684fb413 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 15 Jul 2025 09:29:04 -0300 Subject: [PATCH 12/99] fix: avoid to register routes every time the fn gets called (#36411) --- apps/meteor/app/api/server/api.ts | 7 -- .../federation-matrix/src/FederationMatrix.ts | 65 +++++++++++++++---- .../src/api/.well-known/server.ts | 13 ++-- .../src/api/_matrix/invite.ts | 13 ++-- .../src/api/_matrix/key/server.ts | 13 ++-- .../src/api/_matrix/profiles.ts | 23 ++++--- .../src/api/_matrix/rooms.ts | 15 ++--- .../src/api/_matrix/send-join.ts | 13 ++-- .../src/api/_matrix/transactions.ts | 13 ++-- .../src/api/_matrix/versions.ts | 8 +-- ee/packages/federation-matrix/src/api/api.ts | 31 --------- .../federation-matrix/src/setupContainers.ts | 41 ------------ 12 files changed, 106 insertions(+), 149 deletions(-) delete mode 100644 ee/packages/federation-matrix/src/api/api.ts delete mode 100644 ee/packages/federation-matrix/src/setupContainers.ts diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 74f3ee477bcb8..dbafa020706f8 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -42,9 +42,6 @@ const createApi = function _createApi(options: { version?: string; useDefaultAut export const API: { api: Router<'/api', any, APIActionHandler>; v1: APIClass<'/v1'>; - _matrix: Router<'/_matrix', any, APIActionHandler>; - wellKnown: Router<'/.well-known', any, APIActionHandler>; - matrixInternal: Router<'/internal', any, APIActionHandler>; default: APIClass; ApiClass: typeof APIClass; channels?: { @@ -76,9 +73,6 @@ export const API: { version: 'v1', useDefaultAuth: true, }), - _matrix: new RocketChatAPIRouter('/_matrix'), - wellKnown: new RocketChatAPIRouter('/.well-known'), - matrixInternal: new RocketChatAPIRouter('/internal'), default: createApi({}), }; @@ -106,7 +100,6 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => }); export const startRestAPI = () => { - // Register main API routes under /api prefix (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api .use(remoteAddressMiddleware) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 17ebf9fece781..399377660a86a 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,16 +1,24 @@ import 'reflect-metadata'; -import type { HomeserverEventSignatures, HomeserverServices, DependencyContainer } from '@hs/federation-sdk'; -import { getAllServices } from '@hs/federation-sdk'; +import { toUnpaddedBase64 } from '@hs/core'; +import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; +import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; +import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; -import { getAllMatrixRoutes } from './api/api'; +import { getWellKnownRoutes } from './api/.well-known/server'; +import { getMatrixInviteRoutes } from './api/_matrix/invite'; +import { getKeyServerRoutes } from './api/_matrix/key/server'; +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 { registerEvents } from './events'; -import { setup } from './setupContainers'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -21,10 +29,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS private matrixDomain: string; - private diContainer: DependencyContainer; - private readonly logger = new Logger(this.name); + private httpRoutes: { matrix: Router<'/_matrix'>; wellKnown: Router<'/.well-known'> }; + private constructor(emitter?: Emitter) { super(); this.eventHandler = emitter || new Emitter(); @@ -32,11 +40,48 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS static async create(emitter?: Emitter): Promise { const instance = new FederationMatrix(emitter); - instance.diContainer = await setup(instance.eventHandler); + const config = new ConfigService(); + const matrixConfig = config.getMatrixConfig(); + const serverConfig = config.getServerConfig(); + const signingKeys = await config.getSigningKey(); + const signingKey = signingKeys[0]; + + const containerOptions: FederationContainerOptions = { + emitter, + federationOptions: { + serverName: matrixConfig.serverName, + signingKey: toUnpaddedBase64(signingKey.privateKey), + signingKeyId: `ed25519:${signingKey.version}`, + timeout: 30000, + baseUrl: serverConfig.baseUrl, + }, + }; + + await createFederationContainer(containerOptions); + instance.homeserverServices = getAllServices(); + instance.buildMatrixHTTPRoutes(); return instance; } + private buildMatrixHTTPRoutes() { + const matrix = new Router('/_matrix'); + const wellKnown = new Router('/.well-known'); + + matrix + .use(getMatrixInviteRoutes(this.homeserverServices)) + .use(getMatrixProfilesRoutes(this.homeserverServices)) + .use(getMatrixRoomsRoutes(this.homeserverServices)) + .use(getMatrixSendJoinRoutes(this.homeserverServices)) + .use(getMatrixTransactionsRoutes(this.homeserverServices)) + .use(getKeyServerRoutes(this.homeserverServices)) + .use(getFederationVersionsRoutes()); + + wellKnown.use(getWellKnownRoutes(this.homeserverServices)); + + this.httpRoutes = { matrix, wellKnown }; + } + async created(): Promise { try { registerEvents(this.eventHandler); @@ -58,12 +103,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return this.matrixDomain; } - async started(): Promise { - this.homeserverServices = getAllServices(this.diContainer); - } - getAllRoutes() { - return getAllMatrixRoutes(); + return this.httpRoutes; } async createRoom(room: IRoom, owner: IUser, members: string[]): Promise { diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts index 1deef06292e87..0ab75752a29ce 100644 --- a/ee/packages/federation-matrix/src/api/.well-known/server.ts +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -1,8 +1,9 @@ -import type { Router } from "@rocket.chat/http-router"; +import { Router } from "@rocket.chat/http-router"; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; import { createHash } from 'node:crypto'; +import type { HomeserverServices } from '@hs/federation-sdk'; + const WellKnownServerResponseSchema = { type: 'object', properties: { @@ -16,10 +17,10 @@ const WellKnownServerResponseSchema = { const isWellKnownServerResponseProps = ajv.compile(WellKnownServerResponseSchema); -export const getWellKnownRoutes = (router: Router<'/.well-known'>) => { - const { wellKnown } = getAllServicesFromFederationSDK(); - - return router.get('/matrix/server', { +export const getWellKnownRoutes = (services: HomeserverServices) => { + const { wellKnown } = services; + + return new Router('/matrix').get('/server', { response: { 200: isWellKnownServerResponseProps }, diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index df4438bebd91a..6d957226cb8d8 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; - const EventBaseSchema = { type: 'object', properties: { @@ -127,11 +126,11 @@ const ProcessInviteResponseSchema = { const isProcessInviteResponseProps = ajv.compile(ProcessInviteResponseSchema); -export const getMatrixInviteRoutes = (router: Router<'/_matrix'>) => { - const { invite } = getAllServicesFromFederationSDK(); +export const getMatrixInviteRoutes = (services: HomeserverServices) => { + const { invite } = services; - return router.put( - '/federation/v2/invite/:roomId/:eventId', + return new Router('/federation').put( + '/v2/invite/:roomId/:eventId', { body: isProcessInviteBodyProps, params: isProcessInviteParamsProps, diff --git a/ee/packages/federation-matrix/src/api/_matrix/key/server.ts b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts index af5c76de2e951..a24599cce396f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/key/server.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../../setupContainers'; - const ServerKeyResponseSchema = { type: 'object', properties: { @@ -33,11 +32,11 @@ const ServerKeyResponseSchema = { const isServerKeyResponseProps = ajv.compile(ServerKeyResponseSchema); -export const getKeyServerRoutes = (router: Router<'/_matrix'>) => { - const { server } = getAllServicesFromFederationSDK(); +export const getKeyServerRoutes = (services: HomeserverServices) => { + const { server } = services; - return router.get( - '/key/v2/server', + return new Router('/key').get( + '/v2/server', { response: { 200: isServerKeyResponseProps, diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 400d412c59185..548991adbf71a 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; - const UsernameSchema = { type: 'string', pattern: '^@[A-Za-z0-9_=\\/.+-]+:(.+)$', @@ -330,12 +329,12 @@ const EventAuthResponseSchema = { const isEventAuthResponseProps = ajv.compile(EventAuthResponseSchema); -export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { - const { profile } = getAllServicesFromFederationSDK(); +export const getMatrixProfilesRoutes = (services: HomeserverServices) => { + const { profile } = services; - return router + return new Router('/federation') .get( - '/federation/v1/query/profile', + '/v1/query/profile', { query: isQueryProfileQueryProps, response: { @@ -356,7 +355,7 @@ export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { }, ) .post( - '/federation/v1/user/keys/query', + '/v1/user/keys/query', { body: isQueryKeysBodyProps, response: { @@ -377,7 +376,7 @@ export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { }, ) .get( - '/federation/v1/user/devices/:userId', + '/v1/user/devices/:userId', { params: isGetDevicesParamsProps, response: { @@ -398,7 +397,7 @@ export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { }, ) .get( - '/federation/v1/make_join/:roomId/:userId', + '/v1/make_join/:roomId/:userId', { params: isMakeJoinParamsProps, query: isMakeJoinQueryProps, @@ -438,7 +437,7 @@ export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { }, ) .post( - '/federation/v1/get_missing_events/:roomId', + '/v1/get_missing_events/:roomId', { params: isGetMissingEventsParamsProps, body: isGetMissingEventsBodyProps, @@ -461,7 +460,7 @@ export const getMatrixProfilesRoutes = (router: Router<'/_matrix'>) => { }, ) .get( - '/federation/v1/event_auth/:roomId/:eventId', + '/v1/event_auth/:roomId/:eventId', { params: isEventAuthParamsProps, response: { diff --git a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts index 6670a08108d76..21bc9394b4afe 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; - const PublicRoomsQuerySchema = { type: 'object', properties: { @@ -122,12 +121,12 @@ const PublicRoomsPostBodySchema = { const isPublicRoomsPostBodyProps = ajv.compile(PublicRoomsPostBodySchema); -export const getMatrixRoomsRoutes = (router: Router<'/_matrix'>) => { - const { state } = getAllServicesFromFederationSDK(); +export const getMatrixRoomsRoutes = (services: HomeserverServices) => { + const { state } = services; - return router + return new Router('/federation') .get( - '/federation/v1/publicRooms', + '/v1/publicRooms', { query: isPublicRoomsQueryProps, response: { @@ -158,7 +157,7 @@ export const getMatrixRoomsRoutes = (router: Router<'/_matrix'>) => { }, ) .post( - '/federation/v1/publicRooms', + '/v1/publicRooms', { body: isPublicRoomsPostBodyProps, response: { diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index 9c5d38f77ccb4..842df90db8c5d 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; - const UsernameSchema = { type: 'string', pattern: '^@[A-Za-z0-9_=\\/.+-]+:(.+)$', @@ -216,11 +215,11 @@ const SendJoinResponseSchema = { const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema); -export const getMatrixSendJoinRoutes = (router: Router<'/_matrix'>) => { - const { sendJoin } = getAllServicesFromFederationSDK(); +export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { + const { sendJoin } = services; - return router.put( - '/federation/v2/send_join/:roomId/:stateKey', + return new Router('/federation').put( + '/v2/send_join/:roomId/:stateKey', { params: isSendJoinParamsProps, body: isSendJoinEventProps, diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 93d06bcf6ccf0..9fd59bd2312b4 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -1,8 +1,7 @@ -import type { Router } from '@rocket.chat/http-router'; +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; -import { getAllServicesFromFederationSDK } from '../../setupContainers'; - const SendTransactionParamsSchema = { type: 'object', properties: { @@ -155,11 +154,11 @@ const ErrorResponseSchema = { const isErrorResponseProps = ajv.compile(ErrorResponseSchema); -export const getMatrixTransactionsRoutes = (router: Router<'/_matrix'>) => { - const { event } = getAllServicesFromFederationSDK(); +export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { + const { event } = services; - return router.put( - '/federation/v1/send/:txnId', + return new Router('/federation').put( + '/v1/send/:txnId', { params: isSendTransactionParamsProps, body: isSendTransactionBodyProps, diff --git a/ee/packages/federation-matrix/src/api/_matrix/versions.ts b/ee/packages/federation-matrix/src/api/_matrix/versions.ts index cb039856352bb..49cbce61f6d0c 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/versions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -1,5 +1,5 @@ import { ConfigService } from '@hs/federation-sdk'; -import type { Router } from '@rocket.chat/http-router'; +import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; const GetVersionsResponseSchema = { @@ -25,11 +25,11 @@ const GetVersionsResponseSchema = { const isGetVersionsResponseProps = ajv.compile(GetVersionsResponseSchema); -export const getFederationVersionsRoutes = (router: Router<'/_matrix'>) => { +export const getFederationVersionsRoutes = () => { const configService = new ConfigService(); - return router.get( - '/federation/v1/version', + return new Router('/federation').get( + '/v1/version', { response: { 200: isGetVersionsResponseProps, diff --git a/ee/packages/federation-matrix/src/api/api.ts b/ee/packages/federation-matrix/src/api/api.ts deleted file mode 100644 index e61a602a8a4c1..0000000000000 --- a/ee/packages/federation-matrix/src/api/api.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Router } from '@rocket.chat/http-router'; - -import { getWellKnownRoutes } from './.well-known/server'; -import { getMatrixInviteRoutes } from './_matrix/invite'; -import { getKeyServerRoutes } from './_matrix/key/server'; -import { getMatrixProfilesRoutes } from './_matrix/profiles'; -import { getMatrixRoomsRoutes } from './_matrix/rooms'; -import { getMatrixSendJoinRoutes } from './_matrix/send-join'; -import { getMatrixTransactionsRoutes } from './_matrix/transactions'; -import { getFederationVersionsRoutes } from './_matrix/versions'; - -const matrix = new Router('/_matrix'); -const wellKnown = new Router('/.well-known'); - -export const getAllMatrixRoutes = () => { - matrix - .use(getMatrixInviteRoutes(matrix)) - .use(getMatrixProfilesRoutes(matrix)) - .use(getMatrixRoomsRoutes(matrix)) - .use(getMatrixSendJoinRoutes(matrix)) - .use(getMatrixTransactionsRoutes(matrix)) - .use(getFederationVersionsRoutes(matrix)) - .use(getKeyServerRoutes(matrix)); - - wellKnown.use(getWellKnownRoutes(wellKnown)); - - return { - matrix, - wellKnown, - }; -}; diff --git a/ee/packages/federation-matrix/src/setupContainers.ts b/ee/packages/federation-matrix/src/setupContainers.ts deleted file mode 100644 index 290f3da9d2db1..0000000000000 --- a/ee/packages/federation-matrix/src/setupContainers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'reflect-metadata'; - -import { toUnpaddedBase64 } from '@hs/core'; -import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; -import type { DependencyContainer, FederationContainerOptions, HomeserverEventSignatures, HomeserverServices } from '@hs/federation-sdk'; -import { Emitter } from '@rocket.chat/emitter'; - -let container: DependencyContainer | undefined; - -export async function setup( - emitter: Emitter = new Emitter(), -): Promise { - const config = new ConfigService(); - const matrixConfig = config.getMatrixConfig(); - const serverConfig = config.getServerConfig(); - const signingKeys = await config.getSigningKey(); - const signingKey = signingKeys[0]; - - const containerOptions: FederationContainerOptions = { - emitter, - federationOptions: { - serverName: matrixConfig.serverName, - signingKey: toUnpaddedBase64(signingKey.privateKey), - signingKeyId: `ed25519:${signingKey.version}`, - timeout: 30000, - baseUrl: serverConfig.baseUrl, - }, - }; - - container = createFederationContainer(containerOptions); - - return container; -} - -export function getAllServicesFromFederationSDK(): HomeserverServices { - if (!container) { - throw new Error('Federation container is not initialized. Call setup() first.'); - } - - return getAllServices(container); -} From f4ed218d799eb89f1d275796e2933a1d610c58eb Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Wed, 16 Jul 2025 09:21:42 -0300 Subject: [PATCH 13/99] fix: provide the correct emitter instance --- ee/packages/federation-matrix/src/FederationMatrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 399377660a86a..6380f8a803360 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -47,7 +47,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const signingKey = signingKeys[0]; const containerOptions: FederationContainerOptions = { - emitter, + emitter: instance.eventHandler, federationOptions: { serverName: matrixConfig.serverName, signingKey: toUnpaddedBase64(signingKey.privateKey), From a452883cca58a1428c53c2fa4d8aa095a0fcbd97 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 21 Jul 2025 12:51:58 -0300 Subject: [PATCH 14/99] feat: adds license validation to federation packages and services (#36391) --- apps/meteor/ee/server/startup/federation.ts | 78 +++++++++++++++++++++ apps/meteor/server/services/startup.ts | 7 -- apps/meteor/startRocketChat.ts | 7 ++ ee/apps/federation-service/package.json | 2 + ee/apps/federation-service/src/service.ts | 37 ++++++++-- packages/core-services/src/LocalBroker.ts | 2 + yarn.lock | 2 + 7 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 apps/meteor/ee/server/startup/federation.ts diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts new file mode 100644 index 0000000000000..3a9d8b196cdc4 --- /dev/null +++ b/apps/meteor/ee/server/startup/federation.ts @@ -0,0 +1,78 @@ +import { api } from '@rocket.chat/core-services'; +import { FederationMatrix } from '@rocket.chat/federation-matrix'; +import { License } from '@rocket.chat/license'; +import { Logger } from '@rocket.chat/logger'; + +import { settings } from '../../../app/settings/server'; +import { registerFederationRoutes } from '../api/federation'; + +const logger = new Logger('Federation'); + +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; + return hasLicense && isEnabled; + }; + + 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(); + + 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(); + } + }); +}; diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 09d70e4008f32..2a334932d9530 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -31,7 +31,6 @@ import { UploadService } from './upload/service'; import { UserService } from './user/service'; import { VideoConfService } from './video-conference/service'; import { VoipAsteriskService } from './voip-asterisk/service'; -import { registerFederationRoutes } from '../../ee/server/api/federation'; import { i18n } from '../lib/i18n'; export const registerServices = async (): Promise => { @@ -74,12 +73,6 @@ export const registerServices = async (): Promise => { api.registerService(new Presence()); api.registerService(new Authorization()); - // TODO: Add it to a proper place since it's EE only - const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); - const federationMatrix = await FederationMatrix.create(); - api.registerService(federationMatrix); - await registerFederationRoutes(federationMatrix); - // Run EE services defined outside of the main repo // Otherwise, monolith would ignore them :( // Always register the service and manage licensing inside the service (tbd) 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/federation-service/package.json b/ee/apps/federation-service/package.json index 411b00d1784a8..ba32bc0a0ab59 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -27,7 +27,9 @@ "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:*", + "@rocket.chat/network-broker": "workspace:^", "hono": "^3.11.0", "pino": "^8.16.0", "polka": "^0.5.2", diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 46e69b868dff9..1c4ee6a9e0f0f 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; import { serve } from '@hono/node-server'; -import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection, Settings } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { registerServiceModels } from '@rocket.chat/models'; import { startBroker } from '@rocket.chat/network-broker'; import { Hono } from 'hono'; @@ -10,10 +11,19 @@ import { config } from './config'; function handleHealthCheck(app: Hono) { app.get('/health', async (c) => { try { - return c.json({ status: 'ok' }); + 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' }, 500); + return c.json({ status: 'not healthy', error: (err as Error).message }, 500); } }); } @@ -26,6 +36,18 @@ function handleHealthCheck(app: Hono) { 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(); api.registerService(federationMatrix); @@ -35,13 +57,14 @@ function handleHealthCheck(app: Hono) { app.mount('/_matrix', matrix.getHonoRouter().fetch); app.mount('/.well-known', wellKnown.getHonoRouter().fetch); - + handleHealthCheck(app); serve({ fetch: app.fetch, port: config.port, }); - - await api.start(); -})(); +})().catch((error) => { + console.error('Failed to start service:', error); + process.exit(1); +}); 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/yarn.lock b/yarn.lock index de2637f1f6f7f..7d2f5c141d6a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7753,7 +7753,9 @@ __metadata: "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" "@rocket.chat/http-router": "workspace:*" + "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:*" + "@rocket.chat/network-broker": "workspace:^" "@types/bun": "npm:latest" "@types/express": "npm:^4.17.17" hono: "npm:^3.11.0" From cc3abd9ec43d920da394871bc61976bd0518d81c Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 28 Jul 2025 13:34:14 -0300 Subject: [PATCH 15/99] chore: federation messages using new state resolution (#36548) Co-authored-by: Debdut Chakraborty --- .../federation-matrix/src/FederationMatrix.ts | 12 +++++++----- .../src/api/_matrix/invite.ts | 4 ++-- .../src/api/_matrix/profiles.ts | 18 +++--------------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 6380f8a803360..351a35e665a97 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -112,19 +112,21 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.warn('Homeserver services not available, skipping room creation'); return; } + + if (!(room.t === 'c' || room.t === 'p')) { + throw new Error('Room is not a public or private room'); + } try { const matrixDomain = await this.getMatrixDomain(); const matrixUserId = `@${owner.username}:${matrixDomain}`; const roomName = room.name || room.fname || 'Untitled Room'; - const canonicalAlias = room.fname ? `#${room.fname}:${matrixDomain}` : undefined; + // canonical alias computed from name const matrixRoomResult = await this.homeserverServices.room.createRoom( - matrixUserId, matrixUserId, roomName, - canonicalAlias, - canonicalAlias, + room.t === 'c' ? 'public' : 'invite', ); this.logger.debug('Matrix room created:', matrixRoomResult); @@ -152,7 +154,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS // 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, roomName); + await this.homeserverServices.invite.inviteUserToRoom(member, matrixRoomResult.room_id, matrixUserId); } this.logger.debug('Room creation completed successfully', room._id); diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 6d957226cb8d8..5e6ad5a1c0a0f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -142,9 +142,9 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { }, async (c) => { const { roomId, eventId } = c.req.param(); - const body = await c.req.json(); + const { event, room_version } = await c.req.json(); - const response = await invite.processInvite(body, roomId, eventId); + const response = await invite.processInvite(event, roomId, eventId, room_version); return { body: response, diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 548991adbf71a..3754a7e93d265 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -1,4 +1,5 @@ import type { HomeserverServices } from '@hs/federation-sdk'; +import type { RoomVersion } from '@hs/room'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -412,25 +413,12 @@ export const getMatrixProfilesRoutes = (services: HomeserverServices) => { const url = new URL(c.req.url); const verParams = url.searchParams.getAll('ver'); - const response = await profile.makeJoin(roomId, userId, verParams.length > 0 ? verParams : ['1']); + const response = await profile.makeJoin(roomId, userId, verParams.length > 0 ? verParams as RoomVersion[] : ['1']); return { body: { room_version: response.room_version, - event: { - ...response.event, - content: { - ...response.event.content, - membership: 'join', - join_authorised_via_users_server: response.event.content.join_authorised_via_users_server, - }, - room_id: response.event.room_id, - sender: response.event.sender, - state_key: response.event.state_key, - type: 'm.room.member', - origin_server_ts: response.event.origin_server_ts, - origin: response.event.origin, - }, + event: response.event, }, statusCode: 200, }; From 63d092db9dbe256e973620fcaf239d8d075a4bdb Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 29 Jul 2025 19:45:55 -0300 Subject: [PATCH 16/99] refactor: provide env vars to federation sdk instead of loading them internally (#36557) --- .../federation-matrix/src/FederationMatrix.ts | 25 ++++++++++++++++--- .../src/api/_matrix/versions.ts | 12 ++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 351a35e665a97..a4b35262f5e27 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -40,7 +40,26 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS static async create(emitter?: Emitter): Promise { const instance = new FederationMatrix(emitter); - const config = new ConfigService(); + const config = new ConfigService({ + database: { + uri: process.env.MONGODB_URI || 'mongodb://localhost:3001/meteor', + name: process.env.DATABASE_NAME || 'meteor', + poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10), + }, + server: { + name: process.env.SERVER_NAME || 'rc1', + version: process.env.SERVER_VERSION || '1.0', + port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), + baseUrl: process.env.SERVER_BASE_URL || 'http://rc1:8080', + host: process.env.SERVER_HOST || '0.0.0.0', + }, + matrix: { + serverName: process.env.MATRIX_SERVER_NAME || 'rc1', + domain: process.env.MATRIX_DOMAIN || 'rc1', + keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), + }, + signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', + }); const matrixConfig = config.getMatrixConfig(); const serverConfig = config.getServerConfig(); const signingKeys = await config.getSigningKey(); @@ -57,7 +76,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS }, }; - await createFederationContainer(containerOptions); + await createFederationContainer(containerOptions, config); instance.homeserverServices = getAllServices(); instance.buildMatrixHTTPRoutes(); @@ -75,7 +94,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS .use(getMatrixSendJoinRoutes(this.homeserverServices)) .use(getMatrixTransactionsRoutes(this.homeserverServices)) .use(getKeyServerRoutes(this.homeserverServices)) - .use(getFederationVersionsRoutes()); + .use(getFederationVersionsRoutes(this.homeserverServices)); wellKnown.use(getWellKnownRoutes(this.homeserverServices)); diff --git a/ee/packages/federation-matrix/src/api/_matrix/versions.ts b/ee/packages/federation-matrix/src/api/_matrix/versions.ts index 49cbce61f6d0c..7b2dcc280cb9d 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/versions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -1,4 +1,4 @@ -import { ConfigService } from '@hs/federation-sdk'; +import type { HomeserverServices } from '@hs/federation-sdk'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -25,8 +25,8 @@ const GetVersionsResponseSchema = { const isGetVersionsResponseProps = ajv.compile(GetVersionsResponseSchema); -export const getFederationVersionsRoutes = () => { - const configService = new ConfigService(); +export const getFederationVersionsRoutes = (services: HomeserverServices) => { + const { config } = services; return new Router('/federation').get( '/v1/version', @@ -38,12 +38,12 @@ export const getFederationVersionsRoutes = () => { license: ['federation'], }, async () => { - const config = configService.getServerConfig(); + const serverConfig = config.getServerConfig(); const response = { server: { - name: config.name, - version: config.version, + name: serverConfig.name, + version: serverConfig.version, }, }; From e7fee3404e2accb6b6a140f6caa2ec4eb4faf5a0 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Thu, 31 Jul 2025 09:55:40 -0300 Subject: [PATCH 17/99] chore: support for the server key via settings (#36551) --- .../server/settings/federation-service.ts | 7 ++++ .../federation-matrix/src/FederationMatrix.ts | 35 +++++-------------- packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 3ee05dcdd3003..75271da3e2dcc 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -24,5 +24,12 @@ export const createFederationServiceSettings = async (): Promise => { public: true, alert: 'Federation_Service_Matrix_Port_Alert', }); + + await this.add('Federation_Service_Matrix_Signing_Key', '', { + type: 'string', + i18nLabel: 'Federation_Service_Matrix_Signing_Key', + i18nDescription: 'Federation_Service_Matrix_Signing_Key_Description', + public: false, + }); }); }; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index a4b35262f5e27..726f0424d6f0a 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,6 +1,5 @@ import 'reflect-metadata'; -import { toUnpaddedBase64 } from '@hs/core'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; @@ -40,40 +39,24 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS static async create(emitter?: Emitter): Promise { const instance = new FederationMatrix(emitter); + const settingsSigningKey = await Settings.get('Federation_Service_Matrix_Signing_Key'); const config = new ConfigService({ + serverName: process.env.MATRIX_SERVER_NAME || 'rc1', + keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), + matrixDomain: process.env.MATRIX_DOMAIN || 'rc1', + version: process.env.SERVER_VERSION || '1.0', + port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), + signingKey: settingsSigningKey, + signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', database: { uri: process.env.MONGODB_URI || 'mongodb://localhost:3001/meteor', name: process.env.DATABASE_NAME || 'meteor', poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10), }, - server: { - name: process.env.SERVER_NAME || 'rc1', - version: process.env.SERVER_VERSION || '1.0', - port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), - baseUrl: process.env.SERVER_BASE_URL || 'http://rc1:8080', - host: process.env.SERVER_HOST || '0.0.0.0', - }, - matrix: { - serverName: process.env.MATRIX_SERVER_NAME || 'rc1', - domain: process.env.MATRIX_DOMAIN || 'rc1', - keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), - }, - signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', }); - const matrixConfig = config.getMatrixConfig(); - const serverConfig = config.getServerConfig(); - const signingKeys = await config.getSigningKey(); - const signingKey = signingKeys[0]; const containerOptions: FederationContainerOptions = { emitter: instance.eventHandler, - federationOptions: { - serverName: matrixConfig.serverName, - signingKey: toUnpaddedBase64(signingKey.privateKey), - signingKeyId: `ed25519:${signingKey.version}`, - timeout: 30000, - baseUrl: serverConfig.baseUrl, - }, }; await createFederationContainer(containerOptions, config); @@ -131,7 +114,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.warn('Homeserver services not available, skipping room creation'); return; } - + if (!(room.t === 'c' || room.t === 'p')) { throw new Error('Room is not a public or private room'); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 49efee75ed9ed..91bc949433346 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2161,6 +2161,8 @@ "Federation_Service_Matrix_Port_Description": "The port of the Matrix server to use for federation.", "Federation_Service_Matrix_Port_Alert": "If you're using a DNS or a reverse proxy, you should set this to the port of the DNS handling the federation traffic. E.g. your server is running on port 3000 and you're using a DNS to handle incoming traffic from port 3000 to the DNS name rc1.server.com only. In this case, you should set this to 443.", "Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.", + "Federation_Service_Matrix_Signing_Key": "Matrix server signing key", + "Federation_Service_Matrix_Signing_Key_Description": "The private signing key used by your Matrix server to authenticate federation requests. Format should be: algorithm version base64. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated Matrix servers and should be kept confidential.", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", From 1b579cd848c898be43729a60f84af5a2734d85b5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 15:06:54 -0300 Subject: [PATCH 18/99] feat: federation message reactions (#36420) --- .../ee/server/hooks/federation/index.ts | 28 +++- apps/meteor/ee/server/index.ts | 2 +- ee/apps/federation-service/src/service.ts | 2 +- ee/packages/federation-matrix/package.json | 2 + .../federation-matrix/src/FederationMatrix.ts | 135 +++++++++++++++--- .../federation-matrix/src/events/index.ts | 2 + .../federation-matrix/src/events/reaction.ts | 91 ++++++++++++ .../federation-matrix/src/types/ICallbacks.ts | 18 +++ .../src/utils/emojiConverter.ts | 45 ++++++ .../src/types/IFederationMatrixService.ts | 3 + yarn.lock | 2 + 11 files changed, 307 insertions(+), 23 deletions(-) create mode 100644 ee/packages/federation-matrix/src/events/reaction.ts create mode 100644 ee/packages/federation-matrix/src/types/ICallbacks.ts create mode 100644 ee/packages/federation-matrix/src/utils/emojiConverter.ts diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index b96b8a5f1d21e..d295b50775153 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,6 +1,5 @@ -// import { FederationMatrix } from '@rocket.chat/core-services'; - import { FederationMatrix } from '@rocket.chat/core-services'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; import { getFederationVersion } from '../../../../server/services/federation/utils'; @@ -37,3 +36,28 @@ callbacks.add( callbacks.priority.HIGH, 'federation-v2-after-room-message-sent', ); +callbacks.add( + 'afterSetReaction', + async (message: IMessage, params: { user: IUser; reaction: string }): Promise => { + // Don't federate reactions that came from Matrix + if (params.user.username?.includes(':')) { + return; + } + await FederationMatrix.sendReaction(message._id, params.reaction, params.user); + }, + callbacks.priority.HIGH, + 'federation-matrix-after-set-reaction', +); + +callbacks.add( + 'afterUnsetReaction', + async (_message: IMessage, params: { user: IUser; reaction: string; oldMessage: IMessage }): Promise => { + // Don't federate reactions that came from Matrix + if (params.user.username?.includes(':')) { + return; + } + await FederationMatrix.removeReaction(params.oldMessage._id, params.reaction, params.user, params.oldMessage); + }, + callbacks.priority.HIGH, + 'federation-matrix-after-unset-reaction', +); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 826639c03efb3..e3604bbb36141 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -13,7 +13,7 @@ import './configuration/index'; import './local-services/ldap/service'; import './methods/getReadReceipts'; import './patches'; -// import './hooks/federation'; +import './hooks/federation'; export * from './apps/startup'; export { registerEEBroker } from './startup'; diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 1c4ee6a9e0f0f..702b9f53a10d4 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -57,7 +57,7 @@ function handleHealthCheck(app: Hono) { app.mount('/_matrix', matrix.getHonoRouter().fetch); app.mount('/.well-known', wellKnown.getHonoRouter().fetch); - + handleHealthCheck(app); serve({ diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 89327fe46a086..e99bc461b7316 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -8,6 +8,7 @@ "@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", "babel-jest": "~30.0.0", "eslint": "~8.45.0", @@ -40,6 +41,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", + "emojione": "^4.5.0", "mongodb": "6.10.0", "pino": "8.21.0", "reflect-metadata": "^0.2.2" diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 726f0424d6f0a..ed347aff4b40a 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -7,7 +7,8 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; +import { MatrixBridgedUser, MatrixBridgedRoom, Users, Messages } from '@rocket.chat/models'; +import emojione from 'emojione'; import { getWellKnownRoutes } from './api/.well-known/server'; import { getMatrixInviteRoutes } from './api/_matrix/invite'; @@ -59,7 +60,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS emitter: instance.eventHandler, }; - await createFederationContainer(containerOptions, config); + createFederationContainer(containerOptions, config); instance.homeserverServices = getAllServices(); instance.buildMatrixHTTPRoutes(); @@ -125,11 +126,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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', - ); + const matrixRoomResult = await this.homeserverServices.room.createRoom(matrixUserId, roomName, room.t === 'c' ? 'public' : 'invite'); this.logger.debug('Matrix room created:', matrixRoomResult); @@ -152,7 +149,6 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } catch (error) { this.logger.error('Error creating or updating bridged user:', error); } - // We are not generating bridged users for members outside of the current workspace // They will be created when the invite is accepted @@ -178,30 +174,131 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const matrixUserId = `@${user.username}:${matrixDomain}`; const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); if (!existingMatrixUserId) { - const port = await Settings.get('Federation_Service_Matrix_Port'); - const domain = await Settings.get('Federation_Service_Matrix_Domain'); - const matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain); } - // TODO: We should fix this to not hardcode neither inform the target server - // This is on the homeserver mandate to track all the eligible servers in the federated room - const targetServer = 'hs1-garim.tunnel.dev.rocket.chat'; - if (!this.homeserverServices) { this.logger.warn('Homeserver services not available, skipping message send'); return; } - const result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, matrixUserId, targetServer); + const actualMatrixUserId = existingMatrixUserId || matrixUserId; - // TODO: Store the event ID mapping for future reference (edits, deletions, etc.) - // This would allow us to map between Rocket.Chat message IDs and Matrix event IDs + const result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); - this.logger.debug('Message sent to Matrix successfully:', result.event_id); + 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; } } + + 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + 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 existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); + if (!existingMatrixUserId) { + this.logger.error(`No Matrix user ID mapping found for user ${user._id}`); + return; + } + + const eventId = await this.homeserverServices.message.sendReaction(matrixRoomId, matrixEventId, reactionKey, existingMatrixUserId); + + 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + this.logger.error(`No Matrix room mapping found for room ${message.rid}`); + return; + } + + const reactionKey = emojione.shortnameToUnicode(reaction); + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); + if (!existingMatrixUserId) { + this.logger.error(`No Matrix user ID mapping found for user ${user._id}`); + return; + } + + 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( + matrixRoomId, + eventId, + reactionKey, + existingMatrixUserId, + ); + 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: string): 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; + } + } } diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index c0c2747f3baf5..f65b8c7053bd9 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -4,9 +4,11 @@ import type { Emitter } from '@rocket.chat/emitter'; import { invite } from './invite'; import { message } from './message'; import { ping } from './ping'; +import { reaction } from './reaction'; export function registerEvents(emitter: Emitter) { ping(emitter); message(emitter); invite(emitter); + reaction(emitter); } 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..a2390f6139ccb --- /dev/null +++ b/ee/packages/federation-matrix/src/events/reaction.ts @@ -0,0 +1,91 @@ +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import { Message, FederationMatrix } from '@rocket.chat/core-services'; +import type { Emitter } from '@rocket.chat/emitter'; +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 user = await Users.findOneByUsername(data.sender); + if (!user) { + logger.error(`No RC user mapping found for Matrix event ${reactionTargetEventId} ${data.sender}`); + 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(data.sender, 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 user = await Users.findOneByUsername(data.sender); + if (!user) { + logger.debug(`User not found: ${data.sender}`); + 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/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/src/utils/emojiConverter.ts b/ee/packages/federation-matrix/src/utils/emojiConverter.ts new file mode 100644 index 0000000000000..f73483380343e --- /dev/null +++ b/ee/packages/federation-matrix/src/utils/emojiConverter.ts @@ -0,0 +1,45 @@ +const EMOJI_MAP: Record = { + ':thumbsup:': '👍', + ':thumbsdown:': '👎', + ':heart:': '❤️', + ':smile:': '😊', + ':laughing:': '😂', + ':cry:': '😢', + ':angry:': '😠', + ':star:': '⭐', + ':fire:': '🔥', + ':clap:': '👏', + ':ok_hand:': '👌', + ':wave:': '👋', + ':+1:': '👍', + ':-1:': '👎', + ':100:': '💯', + ':rocket:': '🚀', + ':eyes:': '👀', + ':thinking:': '🤔', + ':party:': '🎉', + ':tada:': '🎉', +}; + +export function convertEmojiToUnicode(reaction: string): string { + if (!reaction.startsWith(':') || !reaction.endsWith(':')) { + return reaction; + } + + const unicode = EMOJI_MAP[reaction]; + if (unicode) { + return unicode; + } + + return reaction.slice(1, -1); +} + +export function convertUnicodeToEmoji(unicode: string): string { + for (const [shortcode, emoji] of Object.entries(EMOJI_MAP)) { + if (emoji === unicode) { + return shortcode; + } + } + + return unicode; +} diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 02bd33f95eebf..05e37f2c976c2 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -17,4 +17,7 @@ export interface IFederationMatrixService { }; createRoom(room: IRoom, owner: IUser, members: string[]): Promise; sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; + sendReaction(messageId: string, reaction: string, user: IUser): Promise; + removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; + getEventById(eventId: string): Promise; } diff --git a/yarn.lock b/yarn.lock index 7d2f5c141d6a5..0c4f48aa603d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7731,8 +7731,10 @@ __metadata: "@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" babel-jest: "npm:~30.0.0" + emojione: "npm:^4.5.0" eslint: "npm:~8.45.0" jest: "npm:~30.0.0" mongodb: "npm:6.10.0" From 633c71082d67b9033bde035b14ef4f6960a4fb00 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 11:23:35 -0300 Subject: [PATCH 19/99] feat: support for inviting users using the add members tab (#36447) --- .../app/lib/server/functions/addUserToRoom.ts | 2 +- .../ee/server/hooks/federation/index.ts | 27 +++++++++- apps/meteor/ee/server/index.ts | 6 ++- .../federation-matrix/src/FederationMatrix.ts | 53 +++++++++++++++++-- .../src/api/_matrix/invite.ts | 4 +- .../src/api/_matrix/profiles.ts | 2 +- .../federation-matrix/src/events/invite.ts | 9 ++-- .../federation-matrix/src/events/message.ts | 2 +- .../src/helpers/identifiers.ts | 1 + 9 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 ee/packages/federation-matrix/src/helpers/identifiers.ts diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 2b477c694c5d0..3820068d76668 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'; diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index d295b50775153..6dc1ff6576e01 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,7 +1,11 @@ import { FederationMatrix } from '@rocket.chat/core-services'; -import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import { isRoomNativeFederated, type IMessage, type IUser } from '@rocket.chat/core-typings'; 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 { getFederationVersion } from '../../../../server/services/federation/utils'; // callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); @@ -36,6 +40,27 @@ callbacks.add( callbacks.priority.HIGH, 'federation-v2-after-room-message-sent', ); +callbacks.add( + 'federation.onAddUsersToRoom', + async ({ invitees, inviter }, room) => FederationMatrix.inviteUsersToRoom(room, invitees, inviter), + callbacks.priority.MEDIUM, + 'native-federation-on-add-users-to-room ', +); + +beforeAddUserToRoom.add( + async ({ user, inviter }, room) => { + if (!user.username || !inviter) { + return; + } + if (!isRoomNativeFederated(room)) { + return; + } + 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: { user: IUser; reaction: string }): Promise => { diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index e3604bbb36141..4a5681de30eec 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -13,7 +13,11 @@ import './configuration/index'; import './local-services/ldap/service'; import './methods/getReadReceipts'; import './patches'; -import './hooks/federation'; +import { License } from '@rocket.chat/license'; export * from './apps/startup'; export { registerEEBroker } from './startup'; + +await License.onLicense('federation', async () => { + await import('./hooks/federation'); +}); diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index ed347aff4b40a..93d1b3ada6068 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -2,12 +2,13 @@ import 'reflect-metadata'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; -import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; +import type { EventID } from '@hs/room'; +import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users, Messages } from '@rocket.chat/models'; +import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages } from '@rocket.chat/models'; import emojione from 'emojione'; import { getWellKnownRoutes } from './api/.well-known/server'; @@ -143,7 +144,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS // TODO: Check if it is external user - split domain etc const localUserId = await Users.findOneByUsername(member); if (localUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, matrixDomain); // continue; } } catch (error) { @@ -195,6 +196,52 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } + async inviteUsersToRoom(room: IRoom, usersUserName: string[], inviter: IUser): Promise { + try { + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${room._id}`); + } + + const matrixDomain = await this.getMatrixDomain(); + const inviterUserId = `@${inviter.username}:${matrixDomain}`; + + await Promise.all( + usersUserName.map(async (username) => { + const alreadyMember = await Subscriptions.findOneByRoomIdAndUsername(room._id, username, { projection: { _id: 1 } }); + if (alreadyMember) { + return; + } + + const isExternalUser = username.includes(':'); + if (isExternalUser) { + let externalUsernameToInvite = username; + const alreadyCreatedLocally = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + if (alreadyCreatedLocally) { + externalUsernameToInvite = `@${username}`; + } + await this.homeserverServices.invite.inviteUserToRoom(externalUsernameToInvite, matrixRoomId, inviterUserId); + return; + } + + const localUser = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + if (localUser) { + await Room.addUserToRoom(room._id, localUser, { _id: inviter._id, username: inviter.username }); + let externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); + if (!externalUserId) { + externalUserId = `@${username}:${matrixDomain}`; + await MatrixBridgedUser.createOrUpdateByLocalId(localUser._id, externalUserId, false, matrixDomain); + } + await this.homeserverServices.invite.inviteUserToRoom(externalUserId, matrixRoomId, inviterUserId); + } + }), + ); + } catch (error) { + this.logger.error('Failed to invite an user to Matrix:', error); + throw error; + } + } + async sendReaction(messageId: string, reaction: string, user: IUser): Promise { try { const message = await Messages.findOneById(messageId); diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 5e6ad5a1c0a0f..eae6b213d5c43 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -142,9 +142,9 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { }, async (c) => { const { roomId, eventId } = c.req.param(); - const { event, room_version } = await c.req.json(); + const { event, room_version: roomVersion } = await c.req.json(); - const response = await invite.processInvite(event, roomId, eventId, room_version); + const response = await invite.processInvite(event, roomId, eventId, roomVersion); return { body: response, diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 3754a7e93d265..759555ae98aa4 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -413,7 +413,7 @@ export const getMatrixProfilesRoutes = (services: HomeserverServices) => { 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']); + const response = await profile.makeJoin(roomId, userId, verParams.length > 0 ? (verParams as RoomVersion[]) : ['1']); return { body: { diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index e1592be26bc4d..ccff963b6f4b0 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -4,6 +4,8 @@ import { UserStatus } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; +import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; + export function invite(emitter: Emitter) { emitter.on('homeserver.matrix.accept-invite', async (data) => { const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); @@ -12,19 +14,20 @@ export function invite(emitter: Emitter) { return; } - const localUser = await Users.findOneByUsername(data.sender); + const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const localUser = await Users.findOneByUsername(internalUsername); if (localUser) { await Room.addUserToRoom(room.rid, localUser); return; } const { insertedId } = await Users.insertOne({ - username: data.sender, + username: internalUsername, type: 'user', status: UserStatus.ONLINE, active: true, roles: ['user'], - name: data.content.displayname || data.sender, + name: data.content.displayname || internalUsername, requirePasswordChange: false, createdAt: new Date(), _updatedAt: new Date(), diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index cc1fdba0eebf3..4d90d77ebd838 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -65,7 +65,7 @@ export function message(emitter: Emitter) { logger.info('Successfully created federated user:', { userId: user._id, username }); } else { - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, data.sender, true, domain); + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, data.sender, false, domain); } const internalRoomId = await MatrixBridgedRoom.getLocalRoomId(data.room_id); diff --git a/ee/packages/federation-matrix/src/helpers/identifiers.ts b/ee/packages/federation-matrix/src/helpers/identifiers.ts new file mode 100644 index 0000000000000..989dfeecd6c73 --- /dev/null +++ b/ee/packages/federation-matrix/src/helpers/identifiers.ts @@ -0,0 +1 @@ +export const convertExternalUserIdToInternalUsername = (externalUserId: string): string => externalUserId.replace(/@/g, ''); From dd6cb6d896b277e579dbce56310f68cb439488af Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Sun, 3 Aug 2025 19:02:29 -0300 Subject: [PATCH 20/99] feat: adds federation kick and leave support (#36572) --- .../app/reactions/server/setReaction.ts | 2 +- .../ee/server/hooks/federation/index.ts | 25 +++++- apps/meteor/ee/server/index.ts | 1 + .../federation-matrix/src/FederationMatrix.ts | 90 ++++++++++++++++++- .../federation-matrix/src/events/index.ts | 2 + .../federation-matrix/src/events/member.ts | 61 +++++++++++++ .../src/types/IFederationMatrixService.ts | 2 + 7 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 ee/packages/federation-matrix/src/events/member.ts diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 53358ac1c94bf..8479ca5c90ae1 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -40,7 +40,7 @@ export async function setReaction( reaction: string, userAlreadyReacted?: boolean, ) { - await Message.beforeReacted(message, room); + // await Message.beforeReacted(message, room); if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 6dc1ff6576e01..bc97b84a96032 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -5,7 +5,6 @@ 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 { getFederationVersion } from '../../../../server/services/federation/utils'; // callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); @@ -86,3 +85,27 @@ callbacks.add( callbacks.priority.HIGH, 'federation-matrix-after-unset-reaction', ); + +afterLeaveRoomCallback.add( + async (user: IUser, room: IRoom): Promise => { + if (!room.federated) { + return; + } + + 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 (!room.federated) { + return; + } + + await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved); + }, + callbacks.priority.HIGH, + 'federation-matrix-after-remove-from-room', +); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 4a5681de30eec..2ae1821824806 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'; import { License } from '@rocket.chat/license'; export * from './apps/startup'; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 93d1b3ada6068..434693a021c6a 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -8,7 +8,7 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages } from '@rocket.chat/models'; +import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; import emojione from 'emojione'; import { getWellKnownRoutes } from './api/.well-known/server'; @@ -348,4 +348,92 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw error; } } + + async leaveRoom(roomId: string, user: IUser): Promise { + try { + const room = await Rooms.findOneById(roomId); + if (!room?.federated) { + this.logger.debug(`Room ${roomId} is not federated, skipping leave operation`); + return; + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); + if (!matrixRoomId) { + this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping leave`); + return; + } + + const matrixDomain = await this.getMatrixDomain(); + const matrixUserId = `@${user.username}:${matrixDomain}`; + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); + + if (!existingMatrixUserId) { + // User might not have been bridged yet if they never sent a message + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain); + } + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room leave'); + return; + } + + const actualMatrixUserId = existingMatrixUserId || matrixUserId; + + await this.homeserverServices.room.leaveRoom(matrixRoomId, actualMatrixUserId); + + this.logger.info(`User ${user.username} left Matrix room ${matrixRoomId} successfully`); + } catch (error) { + this.logger.error('Failed to leave room in Matrix:', error); + throw error; + } + } + + async kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise { + try { + const room = await Rooms.findOneById(roomId); + if (!room?.federated) { + this.logger.debug(`Room ${roomId} is not federated, skipping kick operation`); + return; + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); + if (!matrixRoomId) { + this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping kick`); + return; + } + + const matrixDomain = await this.getMatrixDomain(); + + const kickedMatrixUserId = `@${removedUser.username}:${matrixDomain}`; + const existingKickedMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(removedUser._id); + if (!existingKickedMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, matrixDomain); + } + const actualKickedMatrixUserId = existingKickedMatrixUserId || kickedMatrixUserId; + + const senderMatrixUserId = `@${userWhoRemoved.username}:${matrixDomain}`; + const existingSenderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userWhoRemoved._id); + if (!existingSenderMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, matrixDomain); + } + const actualSenderMatrixUserId = existingSenderMatrixUserId || senderMatrixUserId; + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping user kick'); + return; + } + + await this.homeserverServices.room.kickUser( + matrixRoomId, + actualKickedMatrixUserId, + actualSenderMatrixUserId, + `Kicked by ${userWhoRemoved.username}`, + ); + + this.logger.info(`User ${removedUser.username} was kicked from Matrix room ${matrixRoomId} by ${userWhoRemoved.username}`); + } catch (error) { + this.logger.error('Failed to kick user from Matrix room:', error); + throw error; + } + } } diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index f65b8c7053bd9..3c5910aff0767 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -2,6 +2,7 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { Emitter } from '@rocket.chat/emitter'; import { invite } from './invite'; +import { member } from './member'; import { message } from './message'; import { ping } from './ping'; import { reaction } from './reaction'; @@ -11,4 +12,5 @@ export function registerEvents(emitter: Emitter) { message(emitter); invite(emitter); reaction(emitter); + member(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..91f930dd77ab0 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -0,0 +1,61 @@ +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import { Room } from '@rocket.chat/core-services'; +import type { Emitter } from '@rocket.chat/emitter'; +import { Logger } from '@rocket.chat/logger'; +import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; + +const logger = new Logger('federation-matrix:member'); + +export function member(emitter: Emitter) { + emitter.on('homeserver.matrix.membership', async (data) => { + try { + // Only handle leave events (including kicks) + if (data.content.membership !== 'leave') { + logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); + return; + } + + const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); + 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 affectedMatrixUser = await MatrixBridgedUser.findOne({ mui: data.state_key }); + if (!affectedMatrixUser) { + logger.warn(`No bridged user found for Matrix user_id: ${data.state_key}`); + return; + } + + const affectedUser = await Users.findOneById(affectedMatrixUser.uid); + if (!affectedUser) { + logger.error(`No Rocket.Chat user found for bridged user: ${affectedMatrixUser.uid}`); + 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.rid, affectedUser); + logger.info(`User ${affectedUser.username} left room ${room.rid} via Matrix federation`); + } else { + // Kick - find who kicked + const kickerMatrixUser = await MatrixBridgedUser.findOne({ mui: data.sender }); + let kickerUser = null; + if (kickerMatrixUser) { + kickerUser = await Users.findOneById(kickerMatrixUser.uid); + } + + await Room.removeUserFromRoom(room.rid, 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.rid} by ${data.sender} via Matrix federation.${reasonText}`); + } + } catch (error) { + logger.error('Failed to process Matrix membership event:', error); + } + }); +} diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 05e37f2c976c2..89df53ca9fb6e 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -20,4 +20,6 @@ export interface IFederationMatrixService { sendReaction(messageId: string, reaction: string, user: IUser): Promise; removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; getEventById(eventId: string): Promise; + leaveRoom(roomId: string, user: IUser): Promise; + kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise; } From e92b60f74a23e375b283b040907d06a06dbd7c05 Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Mon, 4 Aug 2025 14:31:41 -0300 Subject: [PATCH 21/99] chore: remove unused var (fix lint) --- apps/meteor/app/reactions/server/setReaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 8479ca5c90ae1..cbad6863e2b9f 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api, Message } from '@rocket.chat/core-services'; +import { api } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; From fca6d7fe728f44c3708c6254afacdec95853854f Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 4 Aug 2025 15:50:40 -0300 Subject: [PATCH 22/99] feat: support for redact message from RC to external (#36492) --- .../ee/server/hooks/federation/index.ts | 17 +++++++++ .../federation-matrix/src/FederationMatrix.ts | 35 +++++++++++++++++- .../federation-matrix/src/events/message.ts | 36 +++++++++++++++++-- .../src/types/IFederationMatrixService.ts | 1 + 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index bc97b84a96032..72b82a6dbb1b0 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,5 +1,6 @@ import { FederationMatrix } from '@rocket.chat/core-services'; import { isRoomNativeFederated, type IMessage, type IUser } from '@rocket.chat/core-typings'; +import { Messages } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; @@ -39,6 +40,22 @@ callbacks.add( callbacks.priority.HIGH, 'federation-v2-after-room-message-sent', ); +callbacks.add( + 'afterDeleteMessage', + async (message: IMessage) => { + if (!message.federation?.eventId) { + return; + } + const isEchoMessage = !(await Messages.findOneByFederationId(message.federation?.eventId)); + if (isEchoMessage) { + return; + } + await FederationMatrix.deleteMessage(message); + }, + callbacks.priority.MEDIUM, + 'native-federation-after-delete-message', +); + callbacks.add( 'federation.onAddUsersToRoom', async ({ invitees, inviter }, room) => FederationMatrix.inviteUsersToRoom(room, invitees, inviter), diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 434693a021c6a..23509d6f26877 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -4,7 +4,7 @@ import { ConfigService, createFederationContainer, getAllServices } from '@hs/fe import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isDeletedMessage, isMessageFromMatrixFederation, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; @@ -196,6 +196,39 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } + async deleteMessage(message: IMessage): Promise { + try { + if (!isMessageFromMatrixFederation(message) || isDeletedMessage(message)) { + return; + } + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${message.rid}`); + } + const matrixDomain = await this.getMatrixDomain(); + const matrixUserId = `@${message.u.username}:${matrixDomain}`; + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(message.u._id); + if (!existingMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(message.u._id, matrixUserId, true, matrixDomain); + } + + 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}`); + } + const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId, matrixUserId); + + 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: IRoom, usersUserName: string[], inviter: IUser): Promise { try { const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 4d90d77ebd838..14ec990143de2 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,10 +1,10 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import { Message } from '@rocket.chat/core-services'; +import { FederationMatrix, Message } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; -import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions } from '@rocket.chat/models'; +import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:message'); @@ -126,4 +126,36 @@ export function message(emitter: Emitter) { 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 user = await Users.findOneByUsername(data.sender); + if (!user) { + logger.debug(`User not found: ${data.sender}`); + return; + } + + await Message.deleteMessage(user, rcMessage); + } catch (error) { + logger.error('Failed to process Matrix removal redaction:', error); + } + }); } diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 89df53ca9fb6e..9ed0fb8258a2d 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -17,6 +17,7 @@ export interface IFederationMatrixService { }; createRoom(room: IRoom, owner: IUser, members: string[]): Promise; sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; + deleteMessage(message: IMessage): Promise; sendReaction(messageId: string, reaction: string, user: IUser): Promise; removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; getEventById(eventId: string): Promise; From bde5605bb30dd55dc0da8e2dae949165d19ac75b Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 5 Aug 2025 20:16:30 -0300 Subject: [PATCH 23/99] feat: federation edus (typing, presence) support (#36626) Co-authored-by: Ricardo Garim --- .../ee/server/hooks/federation/index.ts | 24 +++++- apps/meteor/ee/server/index.ts | 3 +- .../modules/listeners/listeners.module.ts | 55 +++++++----- .../federation-matrix/src/FederationMatrix.ts | 55 +++++++++++- .../src/api/_matrix/transactions.ts | 18 ++-- .../federation-matrix/src/events/edu.ts | 84 +++++++++++++++++++ .../federation-matrix/src/events/index.ts | 2 + packages/core-services/src/events/Events.ts | 5 ++ .../src/models/ISubscriptionsModel.ts | 1 + packages/models/src/models/Rooms.ts | 4 + packages/models/src/models/Subscriptions.ts | 37 ++++++++ 11 files changed, 252 insertions(+), 36 deletions(-) create mode 100644 ee/packages/federation-matrix/src/events/edu.ts diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 72b82a6dbb1b0..2089896a189af 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,7 +1,8 @@ import { FederationMatrix } from '@rocket.chat/core-services'; import { isRoomNativeFederated, type IMessage, type IUser } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; +import { Messages, Rooms } from '@rocket.chat/models'; +import notifications from '../../../../app/notifications/server/lib/Notifications'; import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback'; @@ -10,10 +11,12 @@ import { getFederationVersion } from '../../../../server/services/federation/uti // 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 }) => { const federationVersion = getFederationVersion(); if (federationVersion === 'matrix') { await FederationMatrix.createRoom(room, owner, members); + setupTypingEventListenerForRoom(room._id); } }); @@ -126,3 +129,22 @@ afterRemoveFromRoomCallback.add( callbacks.priority.HIGH, 'federation-matrix-after-remove-from-room', ); + +export const setupTypingEventListenerForRoom = (roomId: string): void => { + notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { + if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { + void api.broadcast('user.typing', { + user: { username }, + isTyping: activity.includes('user-typing'), + roomId, + }); + } + }); +}; + +export const setupInternalEDUEventListeners = async () => { + const federatedRooms = await Rooms.findFederatedRooms({ projection: { _id: 1 } }).toArray(); + for (const room of federatedRooms) { + setupTypingEventListenerForRoom(room._id); + } +}; diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 2ae1821824806..960b3b2f66245 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -20,5 +20,6 @@ export * from './apps/startup'; export { registerEEBroker } from './startup'; await License.onLicense('federation', async () => { - await import('./hooks/federation'); + const { setupInternalEDUEventListeners } = await import('./hooks/federation'); + await setupInternalEDUEventListeners(); }); diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index c37b22e0b1aef..49574d964da8e 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -156,28 +156,7 @@ export class ListenersModule { notifications.notifyRoom(rid, 'videoconf', callId); }); - service.onEvent('presence.status', ({ user }) => { - const { _id, username, name, status, statusText, roles } = user; - if (!status || !username) { - return; - } - - notifications.notifyUserInThisInstance(_id, 'userData', { - type: 'updated', - id: _id, - diff: { - status, - ...(statusText && { statusText }), - }, - unset: {}, - }); - - notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); - - if (_id) { - notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); - } - }); + service.onEvent('presence.status', ({ user }) => this.handlePresence({ user }, notifications)); service.onEvent('user.updateCustomStatus', (userStatus) => { notifications.notifyLoggedInThisInstance('updateCustomUserStatus', { @@ -185,6 +164,12 @@ export class ListenersModule { }); }); + service.onEvent('federation-matrix.user.typing', ({ isTyping, roomId, username }) => { + notifications.notifyRoom(roomId, 'user-activity', username, isTyping ? ['user-typing'] : []); + }); + + service.onEvent('federation-matrix.user.presence.status', ({ user }) => this.handlePresence({ user }, notifications)); + service.onEvent('watch.messages', async ({ message }) => { if (!message.rid) { return; @@ -511,4 +496,30 @@ export class ListenersModule { notifications.streamRoomMessage.emit(roomId, acknowledgeMessage); }); } + + private handlePresence( + { user }: { user: Pick }, + notifications: NotificationsModule, + ): void { + const { _id, username, name, status, statusText, roles } = user; + if (!status || !username) { + return; + } + + notifications.notifyUserInThisInstance(_id, 'userData', { + type: 'updated', + id: _id, + diff: { + status, + ...(statusText && { statusText }), + }, + unset: {}, + }); + + notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); + + if (_id) { + notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); + } + } } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 23509d6f26877..6aae659a70809 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,10 +1,11 @@ import 'reflect-metadata'; +import type { PresenceState } from '@hs/core'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; -import { isDeletedMessage, isMessageFromMatrixFederation, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; +import { isDeletedMessage, isMessageFromMatrixFederation, UserStatus, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; @@ -64,6 +65,58 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS createFederationContainer(containerOptions, config); instance.homeserverServices = getAllServices(); instance.buildMatrixHTTPRoutes(); + instance.onEvent('user.typing', async ({ isTyping, roomId, user: { username } }): Promise => { + if (!roomId || !username) { + return; + } + const externalRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); + if (!externalRoomId) { + return; + } + const localUser = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + if (!localUser) { + return; + } + const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); + if (!externalUserId) { + return; + } + void instance.homeserverServices.edu.sendTypingNotification(externalRoomId, externalUserId, isTyping); + }); + instance.onEvent( + 'presence.status', + async ({ user }: { user: Pick }): Promise => { + if (!user.username || !user.status) { + return; + } + const localUser = await Users.findOneByUsername(user.username, { projection: { _id: 1 } }); + if (!localUser) { + return; + } + const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); + if (!externalUserId) { + return; + } + + 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: externalUserId, + presence: statusMap[user.status] || 'offline', + }, + ], + roomsUserIsMemberOf.map(({ externalRoomId }) => externalRoomId), + ); + }, + ); return instance; } diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 9fd59bd2312b4..f6b76acd4a0d5 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -172,19 +172,15 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { async (c) => { const body = await c.req.json(); - const { pdus = [] } = body; - - if (pdus.length === 0) { - return { - body: { - pdus: {}, - edus: {}, - }, - statusCode: 200, - }; + const { pdus = [], edus = [] } = body; + + if (pdus.length > 0) { + await event.processIncomingPDUs(pdus); } - await event.processIncomingPDUs(pdus); + if (edus.length > 0) { + await event.processIncomingEDUs(edus); + } return { body: { 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..425dcb2de8daf --- /dev/null +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -0,0 +1,84 @@ +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import { api } from '@rocket.chat/core-services'; +import { UserStatus } from '@rocket.chat/core-typings'; +import type { Emitter } from '@rocket.chat/emitter'; +import { Logger } from '@rocket.chat/logger'; +import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; + +import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; + +const logger = new Logger('federation-matrix:edu'); + +export const edus = async (emitter: Emitter) => { + emitter.on('homeserver.matrix.typing', async (data) => { + try { + const matrixRoom = await MatrixBridgedRoom.getLocalRoomId(data.room_id); + if (!matrixRoom) { + logger.debug(`No bridged room found for Matrix room_id: ${data.room_id}`); + return; + } + + const matrixUser = await MatrixBridgedUser.findOne({ mui: convertExternalUserIdToInternalUsername(data.user_id) }); + if (!matrixUser) { + logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); + return; + } + + const user = await Users.findOneById(matrixUser.uid, { projection: { _id: 1, username: 1 } }); + if (!user || !user.username) { + logger.debug(`User not found for uid: ${matrixUser.uid}`); + return; + } + + void api.broadcast('federation-matrix.user.typing', { + username: user.username, + isTyping: data.typing, + roomId: matrixRoom, + }); + } catch (error) { + logger.error('Error handling Matrix typing event:', error); + } + }); + + emitter.on('homeserver.matrix.presence', async (data) => { + try { + const matrixUser = await MatrixBridgedUser.findOne({ mui: convertExternalUserIdToInternalUsername(data.user_id) }); + if (!matrixUser) { + logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); + return; + } + const user = await Users.findOneById(matrixUser.uid, { + projection: { _id: 1, username: 1, statusText: 1, roles: 1, name: 1, status: 1 }, + }); + if (!user) { + logger.debug(`User not found for uid: ${matrixUser.uid}`); + return; + } + + const statusMap = { + online: UserStatus.ONLINE, + offline: UserStatus.OFFLINE, + unavailable: UserStatus.AWAY, + }; + + const status = statusMap[data.presence] || UserStatus.OFFLINE; + await Users.updateOne( + { _id: user._id }, + { + $set: { + status, + statusDefault: status, + }, + }, + ); + + const { _id, username, statusText, roles, name } = user; + void api.broadcast('federation-matrix.user.presence.status', { + user: { status, _id, username, statusText, roles, name }, + }); + logger.debug(`Updated presence for user ${matrixUser.uid} 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 index 3c5910aff0767..9d91e2d8ec8a3 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -1,6 +1,7 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { Emitter } from '@rocket.chat/emitter'; +import { edus } from './edu'; import { invite } from './invite'; import { member } from './member'; import { message } from './message'; @@ -13,4 +14,5 @@ export function registerEvents(emitter: Emitter) { invite(emitter); reaction(emitter); member(emitter); + edus(emitter); } diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 09f7855884dfe..01e25a327a959 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -152,6 +152,11 @@ export type EventSignatures = { }): void; 'user.updateCustomStatus'(userStatus: Omit): void; 'user.typing'(data: { user: Partial; isTyping: boolean; roomId: string }): void; + 'federation-matrix.user.typing'(data: { username: string; isTyping: boolean; roomId: string }): void; + 'federation-matrix.user.presence.status'(data: { + user: Pick; + previousStatus?: UserStatus; + }): void; 'user.video-conference'(data: { userId: IUser['_id']; action: string; 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/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index d236dd0bcf5b8..ef0d3e1f9feab 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, diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index 1829bdcb2d94b..dba1abc564daa 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -2056,4 +2056,41 @@ 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, + }, + }, + { + $lookup: { + from: 'rocketchat_matrix_bridged_rooms', + localField: 'rid', + foreignField: 'rid', + as: 'matrixRoom', + }, + }, + { + $project: { + _id: '$rid', + externalRoomId: { $arrayElemAt: ['$matrixRoom.mri', 0] }, + }, + }, + ]); + } } From 43abc5d64d64c766ff1f03d590170a71e1c84f55 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 5 Aug 2025 20:18:11 -0300 Subject: [PATCH 24/99] feat: federation threads messaging (#36624) --- .../server/services/messages/service.ts | 10 +++++- .../federation-matrix/src/FederationMatrix.ts | 36 ++++++++++++++++++- .../federation-matrix/src/events/message.ts | 29 ++++++++------- .../src/types/IMessageService.ts | 2 ++ 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 02de25012eb70..29767fe0f32a5 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -90,13 +90,21 @@ export class MessageService extends ServiceClassInternal implements IMessageServ rid, msg, federation_event_id, + tmid, }: { fromId: string; rid: string; msg: string; federation_event_id: string; + tmid?: string; }): Promise { - return executeSendMessage(fromId, { rid, msg, federation: { eventId: federation_event_id } }); + const threadParams = tmid ? { tmid, tshow: true } : {}; + return executeSendMessage(fromId, { + rid, + msg, + ...threadParams, + federation: { eventId: federation_event_id }, + }); } async sendMessageWithValidation(user: IUser, message: Partial, room: Partial, upsert = false): Promise { diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 6aae659a70809..34d69200903c2 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -238,7 +238,41 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const actualMatrixUserId = existingMatrixUserId || matrixUserId; - const result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); + let result; + + if (!message.tmid) { + result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); + } else { + const threadRootMessage = await Messages.findOneById(message.tmid); + const threadRootEventId = threadRootMessage?.federation?.eventId; + + if (threadRootEventId) { + const latestThreadMessage = await Messages.findOne( + { + 'tmid': message.tmid, + 'federation.eventId': { $exists: true }, + '_id': { $ne: message._id }, // Exclude the current message + }, + { sort: { ts: -1 } }, + ); + const latestThreadEventId = latestThreadMessage?.federation?.eventId; + + result = await this.homeserverServices.message.sendThreadMessage( + matrixRoomId, + message.msg, + actualMatrixUserId, + threadRootEventId, + latestThreadEventId, + ); + } else { + this.logger.warn('Thread root event ID not found, sending as regular message'); + result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); + } + } + + if (!result) { + throw new Error('Failed to send message to Matrix - no result returned'); + } await Messages.setFederationEventIdById(message._id, result.eventId); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 14ec990143de2..dfde54bfa158c 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -11,18 +11,17 @@ const logger = new Logger('federation-matrix:message'); export function message(emitter: Emitter) { emitter.on('homeserver.matrix.message', async (data) => { try { - logger.info('Received Matrix message event:', { - event_id: data.event_id, - room_id: data.room_id, - sender: data.sender, - }); - const message = data.content?.body?.toString(); if (!message) { logger.debug('No message found in event content'); return; } + const content = data.content as any; + const threadRelation = content?.['m.relates_to']; + const isThreadMessage = threadRelation?.rel_type === 'm.thread'; + const threadRootEventId = isThreadMessage ? threadRelation.event_id : undefined; + const [userPart, domain] = data.sender.split(':'); if (!userPart || !domain) { logger.error('Invalid Matrix sender ID format:', data.sender); @@ -108,20 +107,24 @@ export function message(emitter: Emitter) { } } - logger.info('Saving federated message:', { - fromId: user._id, - roomId: internalRoomId, - eventId: data.event_id, - }); + let tmid: string | undefined; + if (isThreadMessage && threadRootEventId) { + const threadRootMessage = await Messages.findOneByFederationId(threadRootEventId); + if (threadRootMessage) { + tmid = threadRootMessage._id; + logger.debug('Found thread root message:', { tmid, threadRootEventId }); + } else { + logger.warn('Thread root message not found for event:', threadRootEventId); + } + } await Message.saveMessageFromFederation({ fromId: user._id, rid: internalRoomId, msg: message, federation_event_id: data.event_id, + tmid, }); - - logger.debug('Successfully processed Matrix message'); } catch (error) { logger.error('Error processing Matrix message:', error); } diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index f0c0a2df8b8b9..0b2e5a743b11f 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -14,11 +14,13 @@ export interface IMessageService { rid, msg, federation_event_id, + tmid, }: { fromId: string; rid: string; msg: string; federation_event_id: string; + tmid?: string; }): Promise; saveSystemMessageAndNotifyUser( type: MessageTypesValues, From 535a4a06e517007772259bf42ebe87978756c0b0 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 5 Aug 2025 21:08:15 -0300 Subject: [PATCH 25/99] fix: messages redaction echo (#36633) --- apps/meteor/ee/server/hooks/federation/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 2089896a189af..3f5f9c3f88dda 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,6 +1,7 @@ import { FederationMatrix } from '@rocket.chat/core-services'; -import { isRoomNativeFederated, type IMessage, type IUser } from '@rocket.chat/core-typings'; -import { Messages, Rooms } from '@rocket.chat/models'; +import { isRoomNativeFederated, type IMessage, type IUser, type IRoom } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; +import { api } from '@rocket.chat/core-services'; import notifications from '../../../../app/notifications/server/lib/Notifications'; import { callbacks } from '../../../../lib/callbacks'; @@ -49,10 +50,12 @@ callbacks.add( if (!message.federation?.eventId) { return; } - const isEchoMessage = !(await Messages.findOneByFederationId(message.federation?.eventId)); - if (isEchoMessage) { + + const isFromExternalUser = message.u?.username?.includes(':'); + if (isFromExternalUser) { return; } + await FederationMatrix.deleteMessage(message); }, callbacks.priority.MEDIUM, From f0c521207791b1e81592720735bf8407e5a5a904 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 6 Aug 2025 10:12:47 -0300 Subject: [PATCH 26/99] feat: edit message (#36619) Co-authored-by: Debdut Chakraborty --- .../ee/server/hooks/federation/index.ts | 31 ++++++++++++++++-- .../federation-matrix/src/FederationMatrix.ts | 32 +++++++++++++++++++ .../federation-matrix/src/events/message.ts | 27 ++++++++++++++++ .../src/types/IFederationMatrixService.ts | 3 +- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 3f5f9c3f88dda..c5e5ef22dc073 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,7 +1,14 @@ -import { FederationMatrix } from '@rocket.chat/core-services'; -import { isRoomNativeFederated, type IMessage, type IUser, type IRoom } from '@rocket.chat/core-typings'; +import { api, FederationMatrix } from '@rocket.chat/core-services'; +import { + isEditedMessage, + isRoomNativeFederated, + isMessageFromMatrixFederation, + isRoomFederated, + type IMessage, + type IRoom, + type IUser, +} from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; -import { api } from '@rocket.chat/core-services'; import notifications from '../../../../app/notifications/server/lib/Notifications'; import { callbacks } from '../../../../lib/callbacks'; @@ -133,6 +140,24 @@ afterRemoveFromRoomCallback.add( 'federation-matrix-after-remove-from-room', ); +callbacks.add( + 'afterSaveMessage', + async (message: IMessage, { room }): Promise => { + if (!room || !isRoomFederated(room) || !message || !isMessageFromMatrixFederation(message)) { + return message; + } + + if (!isEditedMessage(message)) { + return message; + } + + await FederationMatrix.updateMessage(message._id, message.msg, message.u); + return message; + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-message-updated', +); + export const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 34d69200903c2..3e674a14bccf1 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -556,4 +556,36 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw error; } } + + async updateMessage(messageId: string, newContent: string, sender: IUser): Promise { + try { + const message = await Messages.findOneById(messageId); + if (!message) { + throw new Error(`Message ${messageId} not found`); + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + 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 existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(sender._id); + if (!existingMatrixUserId) { + this.logger.error(`No Matrix user ID mapping found for user ${sender._id}`); + return; + } + + const eventId = await this.homeserverServices.message.updateMessage(matrixRoomId, newContent, existingMatrixUserId, matrixEventId); + + this.logger.debug('Message updated in Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to update message in Matrix:', error); + throw error; + } + } } diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index dfde54bfa158c..e73350f87ab6c 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -118,6 +118,33 @@ export function message(emitter: Emitter) { } } + const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; + if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { + logger.debug('Received edited message from Matrix, updating existing message'); + const originalMessage = await Messages.findOneByFederationId(data.content['m.relates_to'].event_id); + if (!originalMessage) { + logger.error('Original message not found for edit:', data.content['m.relates_to'].event_id); + return; + } + if (originalMessage.federation?.eventId !== data.content['m.relates_to'].event_id) { + return; + } + if (originalMessage.msg === data.content['m.new_content']?.body) { + logger.debug('No changes in message content, skipping update'); + return; + } + + await Message.updateMessage( + { + ...originalMessage, + msg: data.content['m.new_content']?.body, + }, + user, + originalMessage, + ); + return; + } + await Message.saveMessageFromFederation({ fromId: user._id, rid: internalRoomId, diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 9ed0fb8258a2d..4bf09c4d2b2bb 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { Router } from '@rocket.chat/http-router'; export interface IRouteContext { @@ -23,4 +23,5 @@ export interface IFederationMatrixService { getEventById(eventId: string): Promise; leaveRoom(roomId: string, user: IUser): Promise; kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise; + updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; } From 07644c9a3106bf0b605acf9f0180ee85a089c7f5 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 6 Aug 2025 10:16:48 -0300 Subject: [PATCH 27/99] fix: use the same username pattern for federated users (#36641) --- .../federation-matrix/src/FederationMatrix.ts | 16 ++++++++++++---- ee/packages/federation-matrix/src/events/edu.ts | 6 ++---- .../federation-matrix/src/events/message.ts | 17 ++++++++++------- .../federation-matrix/src/events/reaction.ts | 14 +++++++++----- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 3e674a14bccf1..bfcb4188d33ef 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -5,7 +5,14 @@ import { ConfigService, createFederationContainer, getAllServices } from '@hs/fe import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; -import { isDeletedMessage, isMessageFromMatrixFederation, UserStatus, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; +import { + isDeletedMessage, + isMessageFromMatrixFederation, + UserStatus, + type IMessage, + type IRoom, + type IUser, +} from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; @@ -21,6 +28,7 @@ import { getMatrixSendJoinRoutes } from './api/_matrix/send-join'; import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; import { getFederationVersionsRoutes } from './api/_matrix/versions'; import { registerEvents } from './events'; +import { convertExternalUserIdToInternalUsername } from './helpers/identifiers'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -195,7 +203,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS try { // TODO: Check if it is external user - split domain etc - const localUserId = await Users.findOneByUsername(member); + const localUserId = await Users.findOneByUsername(convertExternalUserIdToInternalUsername(member)); if (localUserId) { await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, matrixDomain); // continue; @@ -335,8 +343,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const isExternalUser = username.includes(':'); if (isExternalUser) { - let externalUsernameToInvite = username; - const alreadyCreatedLocally = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + let externalUsernameToInvite = convertExternalUserIdToInternalUsername(username); + const alreadyCreatedLocally = await Users.findOneByUsername(externalUsernameToInvite, { projection: { _id: 1 } }); if (alreadyCreatedLocally) { externalUsernameToInvite = `@${username}`; } diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index 425dcb2de8daf..3fab701075a3c 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -5,8 +5,6 @@ import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; -import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; - const logger = new Logger('federation-matrix:edu'); export const edus = async (emitter: Emitter) => { @@ -18,7 +16,7 @@ export const edus = async (emitter: Emitter) => { return; } - const matrixUser = await MatrixBridgedUser.findOne({ mui: convertExternalUserIdToInternalUsername(data.user_id) }); + const matrixUser = await MatrixBridgedUser.findOne({ mui: data.user_id }); if (!matrixUser) { logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); return; @@ -42,7 +40,7 @@ export const edus = async (emitter: Emitter) => { emitter.on('homeserver.matrix.presence', async (data) => { try { - const matrixUser = await MatrixBridgedUser.findOne({ mui: convertExternalUserIdToInternalUsername(data.user_id) }); + const matrixUser = await MatrixBridgedUser.findOne({ mui: data.user_id }); if (!matrixUser) { logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); return; diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index e73350f87ab6c..a4b3421f340c1 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -6,6 +6,8 @@ import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; +import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; + const logger = new Logger('federation-matrix:message'); export function message(emitter: Emitter) { @@ -29,13 +31,14 @@ export function message(emitter: Emitter) { } const username = userPart.substring(1); - let user = await Users.findOneByUsername(data.sender); + const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + let user = await Users.findOneByUsername(internalUsername); if (!user) { - logger.info('Creating new federated user:', { username: data.sender, externalId: data.sender }); + logger.info('Creating new federated user:', { username: internalUsername, externalId: data.sender }); const userData: Partial = { - username: data.sender, + username: internalUsername, name: username, // TODO: Fetch display name from Matrix profile type: 'user', status: UserStatus.ONLINE, @@ -58,7 +61,7 @@ export function message(emitter: Emitter) { user = await Users.findOneById(insertedId); if (!user) { - logger.error('Failed to create user:', data.sender); + logger.error('Failed to create user:', internalUsername); return; } @@ -176,10 +179,10 @@ export function message(emitter: Emitter) { logger.debug(`No RC message found for event ${data.redacts}`); return; } - - const user = await Users.findOneByUsername(data.sender); + const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const user = await Users.findOneByUsername(internalUsername); if (!user) { - logger.debug(`User not found: ${data.sender}`); + logger.debug(`User not found: ${internalUsername}`); return; } diff --git a/ee/packages/federation-matrix/src/events/reaction.ts b/ee/packages/federation-matrix/src/events/reaction.ts index a2390f6139ccb..63a0c9bf647ec 100644 --- a/ee/packages/federation-matrix/src/events/reaction.ts +++ b/ee/packages/federation-matrix/src/events/reaction.ts @@ -5,6 +5,8 @@ import { Logger } from '@rocket.chat/logger'; import { Users, Messages } from '@rocket.chat/models'; // Rooms import emojione from 'emojione'; +import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; + const logger = new Logger('federation-matrix:reaction'); export function reaction(emitter: Emitter) { @@ -21,9 +23,10 @@ export function reaction(emitter: Emitter) { return; } - const user = await Users.findOneByUsername(data.sender); + const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const user = await Users.findOneByUsername(internalUsername); if (!user) { - logger.error(`No RC user mapping found for Matrix event ${reactionTargetEventId} ${data.sender}`); + logger.error(`No RC user mapping found for Matrix event ${reactionTargetEventId} ${internalUsername}`); return; } @@ -40,7 +43,7 @@ export function reaction(emitter: Emitter) { const reactionEmoji = emojione.toShort(reactionKey); await Message.reactToMessage(user._id, reactionEmoji, rcMessage._id, true); - await Messages.setFederationReactionEventId(data.sender, rcMessage._id, reactionEmoji, data.event_id); + await Messages.setFederationReactionEventId(internalUsername, rcMessage._id, reactionEmoji, data.event_id); } catch (error) { logger.error('Failed to process Matrix reaction:', error); } @@ -75,9 +78,10 @@ export function reaction(emitter: Emitter) { return; } - const user = await Users.findOneByUsername(data.sender); + const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const user = await Users.findOneByUsername(internalUsername); if (!user) { - logger.debug(`User not found: ${data.sender}`); + logger.debug(`User not found: ${internalUsername}`); return; } From a229c764d5dc7e58d28706f5ebd743919eca550f Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 12 Aug 2025 13:03:50 -0300 Subject: [PATCH 28/99] feat: support for mentions and quotes on federation (#36669) --- apps/meteor/server/services/meteor/service.ts | 5 + ee/packages/federation-matrix/jest.config.ts | 5 + ee/packages/federation-matrix/package.json | 6 +- .../federation-matrix/src/FederationMatrix.ts | 137 +- .../federation-matrix/src/events/message.ts | 76 +- .../src/helpers/domain.builder.ts | 13 + .../src/helpers/message.parsers.spec.ts | 1732 +++++++++++++++++ .../src/helpers/message.parsers.ts | 255 +++ packages/core-services/src/types/IMeteor.ts | 1 + yarn.lock | 72 + 10 files changed, 2273 insertions(+), 29 deletions(-) create mode 100644 ee/packages/federation-matrix/src/helpers/domain.builder.ts create mode 100644 ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts create mode 100644 ee/packages/federation-matrix/src/helpers/message.parsers.ts 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/ee/packages/federation-matrix/jest.config.ts b/ee/packages/federation-matrix/jest.config.ts index c18c8ae02465c..5ee40fe48b7a3 100644 --- a/ee/packages/federation-matrix/jest.config.ts +++ b/ee/packages/federation-matrix/jest.config.ts @@ -3,4 +3,9 @@ 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 index e99bc461b7316..ef820a4a32143 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -10,6 +10,7 @@ "@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", @@ -41,9 +42,12 @@ "@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": "8.21.0", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "sanitize-html": "^2.17.0" } } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index bfcb4188d33ef..2e23f06300c4c 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -5,14 +5,8 @@ import { ConfigService, createFederationContainer, getAllServices } from '@hs/fe import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; -import { - isDeletedMessage, - isMessageFromMatrixFederation, - UserStatus, - type IMessage, - type IRoom, - type IUser, -} from '@rocket.chat/core-typings'; +import { isDeletedMessage, isMessageFromMatrixFederation, isQuoteAttachment, UserStatus } from '@rocket.chat/core-typings'; +import type { MessageQuoteAttachment, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; @@ -28,7 +22,9 @@ import { getMatrixSendJoinRoutes } from './api/_matrix/send-join'; import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; import { getFederationVersionsRoutes } from './api/_matrix/versions'; import { registerEvents } from './events'; +import { getMatrixLocalDomain } from './helpers/domain.builder'; import { convertExternalUserIdToInternalUsername } from './helpers/identifiers'; +import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -160,10 +156,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return this.matrixDomain; } - const port = await Settings.get('Federation_Service_Matrix_Port'); - const domain = await Settings.get('Federation_Service_Matrix_Domain'); - - this.matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; + this.matrixDomain = await getMatrixLocalDomain(); return this.matrixDomain; } @@ -248,8 +241,27 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS let result; + const parsedMessage = await toExternalMessageFormat({ + message: message.msg, + externalRoomId: matrixRoomId, + homeServerDomain: await this.getMatrixDomain(), + }); if (!message.tmid) { - result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + if (!quoteMessage) { + throw new Error('Failed to retrieve quote message'); + } + result = await this.homeserverServices.message.sendReplyToMessage( + matrixRoomId, + quoteMessage.rawMessage, + quoteMessage.formattedMessage, + quoteMessage.eventToReplyTo, + actualMatrixUserId, + ); + } else { + result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId); + } } else { const threadRootMessage = await Messages.findOneById(message.tmid); const threadRootEventId = threadRootMessage?.federation?.eventId; @@ -265,16 +277,46 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS ); const latestThreadEventId = latestThreadMessage?.federation?.eventId; - result = await this.homeserverServices.message.sendThreadMessage( - matrixRoomId, - message.msg, - actualMatrixUserId, - threadRootEventId, - latestThreadEventId, - ); + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + if (!quoteMessage) { + throw new Error('Failed to retrieve quote message'); + } + result = await this.homeserverServices.message.sendReplyToInsideThreadMessage( + matrixRoomId, + quoteMessage.rawMessage, + quoteMessage.formattedMessage, + actualMatrixUserId, + threadRootEventId, + quoteMessage.eventToReplyTo, + ); + } else { + result = await this.homeserverServices.message.sendThreadMessage( + matrixRoomId, + message.msg, + parsedMessage, + actualMatrixUserId, + threadRootEventId, + latestThreadEventId, + ); + } } else { this.logger.warn('Thread root event ID not found, sending as regular message'); - result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, actualMatrixUserId); + if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + if (!quoteMessage) { + throw new Error('Failed to retrieve quote message'); + } + result = await this.homeserverServices.message.sendReplyToMessage( + matrixRoomId, + quoteMessage.rawMessage, + quoteMessage.formattedMessage, + quoteMessage.eventToReplyTo, + actualMatrixUserId, + ); + } else { + result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId); + } } } @@ -291,6 +333,46 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } + 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 || !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(message: IMessage): Promise { try { if (!isMessageFromMatrixFederation(message) || isDeletedMessage(message)) { @@ -588,7 +670,18 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } - const eventId = await this.homeserverServices.message.updateMessage(matrixRoomId, newContent, existingMatrixUserId, matrixEventId); + const parsedMessage = await toExternalMessageFormat({ + message: newContent, + externalRoomId: matrixRoomId, + homeServerDomain: await this.getMatrixDomain(), + }); + const eventId = await this.homeserverServices.message.updateMessage( + matrixRoomId, + newContent, + parsedMessage, + existingMatrixUserId, + matrixEventId, + ); this.logger.debug('Message updated in Matrix successfully:', eventId); } catch (error) { diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index a4b3421f340c1..44e62a1316b3c 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,12 +1,14 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import { FederationMatrix, Message } from '@rocket.chat/core-services'; +import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; +import { getMatrixLocalDomain } from '../helpers/domain.builder'; import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; +import { toInternalMessageFormat, toInternalQuoteMessageFormat } from '../helpers/message.parsers'; const logger = new Logger('federation-matrix:message'); @@ -20,9 +22,10 @@ export function message(emitter: Emitter) { } const content = data.content as any; - const threadRelation = content?.['m.relates_to']; - const isThreadMessage = threadRelation?.rel_type === 'm.thread'; - const threadRootEventId = isThreadMessage ? threadRelation.event_id : undefined; + const replyToRelation = content?.['m.relates_to']; + const isThreadMessage = replyToRelation?.rel_type === 'm.thread'; + const isQuoteMessage = replyToRelation?.['m.in_reply_to']?.event_id && !replyToRelation?.is_falling_back; + const threadRootEventId = isThreadMessage ? replyToRelation.event_id : undefined; const [userPart, domain] = data.sender.split(':'); if (!userPart || !domain) { @@ -121,6 +124,7 @@ export function message(emitter: Emitter) { } } + const localDomain = await getMatrixLocalDomain(); const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { logger.debug('Received edited message from Matrix, updating existing message'); @@ -137,21 +141,81 @@ export function message(emitter: Emitter) { return; } + if (isQuoteMessage && 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: message, + homeServerDomain: localDomain, + 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: localDomain, + senderExternalId: data.sender, + }); await Message.updateMessage( { ...originalMessage, - msg: data.content['m.new_content']?.body, + msg: formatted, }, user, originalMessage, ); return; } + if (isQuoteMessage && room.name) { + const originalMessage = await Messages.findOneByFederationId(replyToRelation?.['m.in_reply_to']?.event_id); + if (!originalMessage) { + logger.error('Original message not found for edit:', replyToRelation?.['m.in_reply_to']?.event_id); + 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: message, + homeServerDomain: localDomain, + senderExternalId: data.sender, + }); + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: internalRoomId, + msg: formatted, + federation_event_id: data.event_id, + tmid, + }); + return; + } + const formatted = await toInternalMessageFormat({ + rawMessage: message, + formattedMessage: data.content.formatted_body || '', + homeServerDomain: localDomain, + senderExternalId: data.sender, + }); await Message.saveMessageFromFederation({ fromId: user._id, rid: internalRoomId, - msg: message, + msg: formatted, federation_event_id: data.event_id, tmid, }); diff --git a/ee/packages/federation-matrix/src/helpers/domain.builder.ts b/ee/packages/federation-matrix/src/helpers/domain.builder.ts new file mode 100644 index 0000000000000..478cc48564c18 --- /dev/null +++ b/ee/packages/federation-matrix/src/helpers/domain.builder.ts @@ -0,0 +1,13 @@ +import { Settings } from '@rocket.chat/models'; + +export const getMatrixLocalDomain = async () => { + const port = await Settings.findOneById('Federation_Service_Matrix_Port'); + const domain = await Settings.findOneById('Federation_Service_Matrix_Domain'); + if (!port || !domain) { + throw new Error('Matrix domain or port not found'); + } + + const matrixDomain = port.value === 443 || port.value === 80 ? domain.value : `${domain.value}:${port.value}`; + + return String(matrixDomain); +}; 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/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/yarn.lock b/yarn.lock index 0c4f48aa603d0..a1b6bd3a7f2c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4128,6 +4128,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" @@ -7733,13 +7743,17 @@ __metadata: "@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:8.21.0" reflect-metadata: "npm:^0.2.2" + sanitize-html: "npm:^2.17.0" typescript: "npm:~5.8.3" languageName: unknown linkType: soft @@ -12847,6 +12861,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" @@ -13713,6 +13736,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" @@ -27041,6 +27090,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" @@ -33161,6 +33219,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" From 44171f23af9eb137ffbc726a943b319e9f70843c Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 13 Aug 2025 15:50:45 +0530 Subject: [PATCH 29/99] feat: name and topic change (#36700) --- .../server/methods/saveRoomSettings.ts | 5 +++ .../ee/server/hooks/federation/index.ts | 22 +++++++++++ apps/meteor/server/services/room/service.ts | 9 +++++ .../federation-matrix/src/FederationMatrix.ts | 39 +++++++++++++++++++ .../federation-matrix/src/events/index.ts | 2 + .../federation-matrix/src/events/room.ts | 38 ++++++++++++++++++ .../src/types/IFederationMatrixService.ts | 2 + .../core-services/src/types/IRoomService.ts | 1 + 8 files changed, 118 insertions(+) create mode 100644 ee/packages/federation-matrix/src/events/room.ts diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 7cbdca852cd6d..745779e2230e1 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -231,6 +231,11 @@ const settingSavers: RoomSettingsSavers = { await saveRoomTopic(rid, value, user); } }, + async sidepanel({ value, rid, room }) { + if (JSON.stringify(value) !== JSON.stringify(room.sidepanel)) { + await Rooms.setSidepanelById(rid, value); + } + }, async roomAnnouncement({ value, room, rid, user }) { if (!value && !room.announcement) { return; diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index c5e5ef22dc073..328fd6fd5000a 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -140,6 +140,28 @@ afterRemoveFromRoomCallback.add( 'federation-matrix-after-remove-from-room', ); +callbacks.add( + 'afterRoomNameChange', + async ({ room, name, userId }) => { + if (name && isRoomFederated(room)) { + await FederationMatrix.updateRoomName(room._id, name, userId); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-name-changed', +); + +callbacks.add( + 'afterRoomTopicChange', + async ({ room, name, userId }) => { + if (name && isRoomFederated(room)) { + await FederationMatrix.updateRoomTopic(room._id, name, userId); + } + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-topic-changed', +); + callbacks.add( 'afterSaveMessage', async (message: IMessage, { room }): Promise => { diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index dfb118cf84758..07926f53a9b1b 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -12,6 +12,7 @@ import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { createDirectMessage } from '../../methods/createDirectMessage'; +import { saveRoomName } from '../../../app/channel-settings/server'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; @@ -142,4 +143,12 @@ 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); + } } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 2e23f06300c4c..3bf0d94d829bc 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -689,4 +689,43 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw error; } } + + async updateRoomName(rid: string, displayName: string, senderId: string): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room name update'); + return; + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${rid}`); + } + + const userId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); + if (!userId) { + throw new Error(`No Matrix user ID mapping found for user ${senderId}`); + } + + await this.homeserverServices.room.updateRoomName(matrixRoomId, displayName, userId); + } + + async updateRoomTopic(rid: string, topic: string, senderId: string): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping room topic update'); + + return; + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${rid}`); + } + + const userId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); + if (!userId) { + throw new Error(`No Matrix user ID mapping found for user ${senderId}`); + } + + await this.homeserverServices.room.setRoomTopic(matrixRoomId, userId, topic); + } } diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index 9d91e2d8ec8a3..4ba1d5673cac1 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -7,6 +7,7 @@ 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) { ping(emitter); @@ -15,4 +16,5 @@ export function registerEvents(emitter: Emitter) { reaction(emitter); member(emitter); edus(emitter); + room(emitter); } 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..4628651dd59a8 --- /dev/null +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -0,0 +1,38 @@ +import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import { MatrixBridgedRoom, MatrixBridgedUser } from '@rocket.chat/models'; +import { Room } from '@rocket.chat/core-services'; + +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 MatrixBridgedRoom.getLocalRoomId(roomId); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + await Room.saveRoomName(localRoomId, localUserId, name); + }); + + emitter.on('homeserver.matrix.room.topic', async (data) => { + const { room_id: roomId, topic, user_id: userId } = data; + + const localRoomId = await MatrixBridgedRoom.getLocalRoomId(roomId); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + await Room.saveRoomTopic(localRoomId, topic, { _id: localUserId, username: userId }); + }); +} diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 4bf09c4d2b2bb..c653124bf9f9f 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -24,4 +24,6 @@ export interface IFederationMatrixService { leaveRoom(roomId: string, user: IUser): Promise; kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise; updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; + updateRoomName(roomId: string, name: string, sender: string): Promise; + updateRoomTopic(roomId: string, topic: string, sender: string): Promise; } diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 70d64dc9966f0..64c7d3683ea76 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -57,4 +57,5 @@ export interface IRoomService { beforeUserRemoved(room: IRoom): Promise; beforeNameChange(room: IRoom): Promise; beforeTopicChange(room: IRoom): Promise; + saveRoomName(roomId: string, userId: string, name: string): Promise; } From 473903f2fc99ac5ffcac2df944b3e7af042329cf Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 15:13:25 -0300 Subject: [PATCH 30/99] feat: auto join rooms on invite (#36729) --- .../ee/server/hooks/federation/index.ts | 21 ++- .../federation-matrix/src/FederationMatrix.ts | 13 +- .../src/api/_matrix/invite.ts | 174 +++++++++++++++++- .../src/api/_matrix/profiles.ts | 10 +- .../src/api/_matrix/send-join.ts | 9 +- .../federation-matrix/src/events/invite.ts | 4 +- .../federation-matrix/src/events/message.ts | 5 +- .../federation-matrix/src/events/reaction.ts | 6 +- .../src/helpers/identifiers.ts | 77 ++++++++ .../core-services/src/types/IRoomService.ts | 1 + 10 files changed, 283 insertions(+), 37 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 328fd6fd5000a..0106b36d33ed9 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -8,7 +8,7 @@ import { type IRoom, type IUser, } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { MatrixBridgedRoom, Rooms } from '@rocket.chat/models'; import notifications from '../../../../app/notifications/server/lib/Notifications'; import { callbacks } from '../../../../lib/callbacks'; @@ -20,11 +20,24 @@ import { getFederationVersion } from '../../../../server/services/federation/uti // 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 }) => { +callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members, options }) => { const federationVersion = getFederationVersion(); - if (federationVersion === 'matrix') { - await FederationMatrix.createRoom(room, owner, members); + if (federationVersion === 'native') { + const federatedRoomId = options?.federatedRoomId; + // TODO: move this to the hooks folder setupTypingEventListenerForRoom(room._id); + + if (!federatedRoomId) { + // if room if 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 MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, federatedRoomId, fromServer); + } + + await Rooms.setAsFederated(room._id); } }); diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 3bf0d94d829bc..191cab6058952 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -23,7 +23,7 @@ import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; import { getFederationVersionsRoutes } from './api/_matrix/versions'; import { registerEvents } from './events'; import { getMatrixLocalDomain } from './helpers/domain.builder'; -import { convertExternalUserIdToInternalUsername } from './helpers/identifiers'; +import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { @@ -187,7 +187,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, matrixDomain); - await MatrixBridgedUser.createOrUpdateByLocalId(owner._id, matrixUserId, true, matrixDomain); + await saveExternalUserIdForLocalUser(owner, matrixUserId); for await (const member of members) { if (member === owner.username) { @@ -196,7 +196,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS try { // TODO: Check if it is external user - split domain etc - const localUserId = await Users.findOneByUsername(convertExternalUserIdToInternalUsername(member)); + const localUserId = await Users.findOneByUsername(member); if (localUserId) { await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, matrixDomain); // continue; @@ -425,12 +425,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const isExternalUser = username.includes(':'); if (isExternalUser) { - let externalUsernameToInvite = convertExternalUserIdToInternalUsername(username); - const alreadyCreatedLocally = await Users.findOneByUsername(externalUsernameToInvite, { projection: { _id: 1 } }); - if (alreadyCreatedLocally) { - externalUsernameToInvite = `@${username}`; - } - await this.homeserverServices.invite.inviteUserToRoom(externalUsernameToInvite, matrixRoomId, inviterUserId); + await this.homeserverServices.invite.inviteUserToRoom(username, matrixRoomId, inviterUserId); return; } diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index eae6b213d5c43..7b525c8ade935 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -1,5 +1,9 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; +import type { HomeserverServices, RoomService, StateService } from '@hs/federation-sdk'; +import type { PersistentEventBase } from '@hs/room'; +import { Room } from '@rocket.chat/core-services'; +import type { IUser, UserStatus } from '@rocket.chat/core-typings'; import { Router } from '@rocket.chat/http-router'; +import { MatrixBridgedRoom, MatrixBridgedUser, Rooms, Users } from '@rocket.chat/models'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; const EventBaseSchema = { @@ -99,8 +103,6 @@ const RoomMemberEventSchema = { ], }; -const isProcessInviteBodyProps = ajv.compile(RoomMemberEventSchema); - const ProcessInviteParamsSchema = { type: 'object', properties: { @@ -126,13 +128,144 @@ const ProcessInviteResponseSchema = { 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 + if (!matrixRoom.isPublic() && !matrixRoom.isInviteOnly()) { + throw new Error('room is neither public not private, rocketchat is unable to join for now'); + } + + // need both the sender and the participating user to exist in the room + const internalSenderUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(inviteEvent.sender); + + let senderUserId: string; + + if (!internalSenderUserId) { + // create locally + // what we were using previously + /* + public getStorageRepresentation(): Readonly { + return { + _id: this.internalId, + username: this.internalReference.username || '', + type: this.internalReference.type, + status: this.internalReference.status, + active: this.internalReference.active, + roles: this.internalReference.roles, + name: this.internalReference.name, + requirePasswordChange: this.internalReference.requirePasswordChange, + createdAt: new Date(), + _updatedAt: new Date(), + federated: this.isRemote(), + }; + } + */ + + const user = { + // 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, + createdAt: new Date(), + _updatedAt: new Date(), + }; + + const createdUser = await Users.insertOne(user); + + senderUserId = createdUser.insertedId; + + await MatrixBridgedUser.createOrUpdateByLocalId(senderUserId, inviteEvent.sender, true, matrixRoom.origin); + } else { + // already got the mapped sender + const user = await Users.findOneById(internalSenderUserId); + if (!user) { + throw new Error('user not found although should have as it is in mapping not processing invite'); + } + + senderUserId = user._id; + } + + let internalRoomId: string; + + const internalMappedRoomId = await MatrixBridgedRoom.getLocalRoomId(inviteEvent.roomId); + + if (!internalMappedRoomId) { + const ourRoom = await Room.create(senderUserId, { + type: matrixRoom.isPublic() ? 'c' : 'p', + name: matrixRoom.name, + options: { + federatedRoomId: inviteEvent.roomId, + creator: senderUserId, + } + }); + + internalRoomId = ourRoom._id; + } else { + const room = await Rooms.findOneById(internalMappedRoomId); + if (!room) { + throw new Error('room not found although should have as it is in mapping not processing invite'); + } + + internalRoomId = room._id; + } + + await Room.addUserToRoom(internalRoomId, { _id: user._id }, { _id: senderUserId, username: inviteEvent.sender }); +} + +async function startJoiningRoom(...opts: Parameters) { + void runWithBackoff(() => joinRoom(...opts)); +} + export const getMatrixInviteRoutes = (services: HomeserverServices) => { - const { invite } = services; + const { invite, state, room } = services; return new Router('/federation').put( '/v2/invite/:roomId/:eventId', { - body: isProcessInviteBodyProps, + body: ajv.compile({ type: 'object' }), // TODO: add schema from room package. params: isProcessInviteParamsProps, response: { 200: isProcessInviteResponseProps, @@ -142,12 +275,37 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { }, async (c) => { const { roomId, eventId } = c.req.param(); - const { event, room_version: roomVersion } = await c.req.json(); + const { event, room_version } = await c.req.json(); + + const userToCheck = event.state_key as string; - const response = await invite.processInvite(event, roomId, eventId, roomVersion); + 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, room_version); + + void startJoiningRoom({ + inviteEvent, + user: ourUser, + room, + state, + }); return { - body: response, + body: { + event: inviteEvent.event, + }, statusCode: 200, }; }, diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 759555ae98aa4..4ca8e6646663c 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -143,6 +143,7 @@ const MakeJoinParamsSchema = { required: ['roomId', 'userId'], }; +// @ts-ignore const isMakeJoinParamsProps = ajv.compile(MakeJoinParamsSchema); const MakeJoinQuerySchema = { @@ -159,6 +160,7 @@ const MakeJoinQuerySchema = { }, }; +// @ts-ignore const isMakeJoinQueryProps = ajv.compile(MakeJoinQuerySchema); const MakeJoinResponseSchema = { @@ -243,6 +245,7 @@ const MakeJoinResponseSchema = { required: ['room_version', 'event'], }; +// @ts-ignore const isMakeJoinResponseProps = ajv.compile(MakeJoinResponseSchema); const GetMissingEventsParamsSchema = { @@ -400,10 +403,11 @@ export const getMatrixProfilesRoutes = (services: HomeserverServices) => { .get( '/v1/make_join/:roomId/:userId', { - params: isMakeJoinParamsProps, - query: isMakeJoinQueryProps, + // TODO: fix types here, likely import from room package + params: ajv.compile({ type: 'object' }), + query: ajv.compile({ type: 'object' }), response: { - 200: isMakeJoinResponseProps, + 200: ajv.compile({ type: 'object' }), }, tags: ['Federation'], license: ['federation'], diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index 842df90db8c5d..3a75458ca74db 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -46,6 +46,7 @@ const SendJoinParamsSchema = { required: ['roomId', 'stateKey'], }; +// @ts-ignore const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema); const EventHashSchema = { @@ -181,6 +182,7 @@ const SendJoinEventSchema = { ], }; +// @ts-ignore const isSendJoinEventProps = ajv.compile(SendJoinEventSchema); const SendJoinResponseSchema = { @@ -213,6 +215,7 @@ const SendJoinResponseSchema = { required: ['event', 'state', 'auth_chain', 'members_omitted', 'origin'], }; +// @ts-ignore const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema); export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { @@ -221,10 +224,10 @@ export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { return new Router('/federation').put( '/v2/send_join/:roomId/:stateKey', { - params: isSendJoinParamsProps, - body: isSendJoinEventProps, + params: ajv.compile({ type: 'object' }), + body: ajv.compile({ type: 'object' }), response: { - 200: isSendJoinResponseProps, + 200: ajv.compile({ type: 'object' }), }, tags: ['Federation'], license: ['federation'], diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index ccff963b6f4b0..314ab87162482 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -4,8 +4,6 @@ import { UserStatus } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; -import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; - export function invite(emitter: Emitter) { emitter.on('homeserver.matrix.accept-invite', async (data) => { const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); @@ -14,7 +12,7 @@ export function invite(emitter: Emitter) { return; } - const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const internalUsername = data.sender; const localUser = await Users.findOneByUsername(internalUsername); if (localUser) { await Room.addUserToRoom(room.rid, localUser); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 44e62a1316b3c..a336cfd1253a2 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -7,7 +7,6 @@ import { Logger } from '@rocket.chat/logger'; import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; import { getMatrixLocalDomain } from '../helpers/domain.builder'; -import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; import { toInternalMessageFormat, toInternalQuoteMessageFormat } from '../helpers/message.parsers'; const logger = new Logger('federation-matrix:message'); @@ -34,7 +33,7 @@ export function message(emitter: Emitter) { } const username = userPart.substring(1); - const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const internalUsername = data.sender; let user = await Users.findOneByUsername(internalUsername); if (!user) { @@ -243,7 +242,7 @@ export function message(emitter: Emitter) { logger.debug(`No RC message found for event ${data.redacts}`); return; } - const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const internalUsername = data.sender; const user = await Users.findOneByUsername(internalUsername); if (!user) { logger.debug(`User not found: ${internalUsername}`); diff --git a/ee/packages/federation-matrix/src/events/reaction.ts b/ee/packages/federation-matrix/src/events/reaction.ts index 63a0c9bf647ec..ee8c3393022ea 100644 --- a/ee/packages/federation-matrix/src/events/reaction.ts +++ b/ee/packages/federation-matrix/src/events/reaction.ts @@ -5,8 +5,6 @@ import { Logger } from '@rocket.chat/logger'; import { Users, Messages } from '@rocket.chat/models'; // Rooms import emojione from 'emojione'; -import { convertExternalUserIdToInternalUsername } from '../helpers/identifiers'; - const logger = new Logger('federation-matrix:reaction'); export function reaction(emitter: Emitter) { @@ -23,7 +21,7 @@ export function reaction(emitter: Emitter) { return; } - const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const internalUsername = data.sender; const user = await Users.findOneByUsername(internalUsername); if (!user) { logger.error(`No RC user mapping found for Matrix event ${reactionTargetEventId} ${internalUsername}`); @@ -78,7 +76,7 @@ export function reaction(emitter: Emitter) { return; } - const internalUsername = convertExternalUserIdToInternalUsername(data.sender); + const internalUsername = data.sender; const user = await Users.findOneByUsername(internalUsername); if (!user) { logger.debug(`User not found: ${internalUsername}`); diff --git a/ee/packages/federation-matrix/src/helpers/identifiers.ts b/ee/packages/federation-matrix/src/helpers/identifiers.ts index 989dfeecd6c73..99957f159348f 100644 --- a/ee/packages/federation-matrix/src/helpers/identifiers.ts +++ b/ee/packages/federation-matrix/src/helpers/identifiers.ts @@ -1 +1,78 @@ +import type { IUser, UserStatus } from '@rocket.chat/core-typings'; +import { MatrixBridgedUser, Users } from '@rocket.chat/models'; + export const convertExternalUserIdToInternalUsername = (externalUserId: string): string => externalUserId.replace(/@/g, ''); + +export const getLocalUsernameForMatrixUserIdToSave = (matrixUserId: string): string => matrixUserId; // TODO: decide on whether to keep @ or not + +export const getLocalNameForMatrixUserIdToSave = (matrixUserId: string): string => + matrixUserId.split(':').shift()?.replace(/@/g, '') as string; + +// can have none if in case of local user +export const getExternalUserIdForLocalUserToSave = (user: IUser): string | undefined => + user.federated ? /* remote user, already has @ according to the function above */ (user.username as string) : undefined; + +export async function getLocalUserForExternalUserId(externalUserId: string): Promise { + const localUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(externalUserId); + if (!localUserId) { + return null; + } + + const user = await Users.findOneById(localUserId); + if (!user) { + throw new Error('user not found although should have as it is in mapping not processing invite'); + } + + return user; +} + +export async function getExternalUserIdForLocalUser(user: IUser): Promise { + const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); + return externalUserId; +} + +export async function saveExternalUserIdForLocalUser(user: IUser, externalUserId: string): Promise { + const matrixDomain = externalUserId.split(':')[1]; + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, externalUserId, true, matrixDomain); +} + +export async function saveLocalUserForExternalUserId(externalUserId: string, origin: string): Promise { + /* + * using from ------- + public getStorageRepresentation(): Readonly { + return { + _id: this.internalId, + username: this.internalReference.username || '', + type: this.internalReference.type, + status: this.internalReference.status, + active: this.internalReference.active, + roles: this.internalReference.roles, + name: this.internalReference.name, + requirePasswordChange: this.internalReference.requirePasswordChange, + createdAt: new Date(), + _updatedAt: new Date(), + federated: this.isRemote(), + }; + } + */ + + const user = { + // let the _id auto generate we deal with usernames + username: getLocalUsernameForMatrixUserIdToSave(externalUserId), + type: 'user', + status: 'online' as UserStatus, + active: true, + roles: ['user'], + name: getLocalNameForMatrixUserIdToSave(externalUserId), + requirePasswordChange: false, + federated: true, + createdAt: new Date(), + _updatedAt: new Date(), + }; + + const { insertedId } = await Users.insertOne(user); + + await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, origin); + + return insertedId; +} diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 64c7d3683ea76..6481fc0a2bed4 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 { From bcf3e1274e0d30c196b6a063cf1d4c20b678dd22 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 18 Aug 2025 13:06:58 +0530 Subject: [PATCH 31/99] feat: change room roles (#36730) --- apps/meteor/app/api/server/v1/channels.ts | 9 ++++ apps/meteor/app/api/server/v1/groups.ts | 9 ++++ .../ee/server/hooks/federation/index.ts | 4 ++ apps/meteor/lib/callbacks.ts | 1 + .../server/services/federation/utils.ts | 8 +++ apps/meteor/server/services/room/service.ts | 51 ++++++++++++++++++- .../federation-matrix/src/FederationMatrix.ts | 30 +++++++++++ .../federation-matrix/src/events/room.ts | 28 +++++++++- .../src/types/IFederationMatrixService.ts | 1 + .../core-services/src/types/IRoomService.ts | 1 + 10 files changed, 140 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index b5574e239ee4b..2a5a9ba5df7e8 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -57,6 +57,7 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; +import { callbacks } from '../../../../lib/callbacks'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property async function findChannelByIdOrName({ @@ -562,6 +563,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'moderator' }); + await addRoomModerator(this.userId, findResult._id, user._id); return API.v1.success(); @@ -578,6 +581,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'owner' }); + await addRoomOwner(this.userId, findResult._id, user._id); return API.v1.success(); @@ -1186,6 +1191,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'user' }); + await removeRoomModerator(this.userId, findResult._id, user._id); return API.v1.success(); @@ -1202,6 +1209,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'user' }); + await removeRoomOwner(this.userId, findResult._id, user._id); return API.v1.success(); diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 311e383232171..784b3ad8d4d76 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -37,6 +37,7 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; +import { callbacks } from '../../../../lib/callbacks'; async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( @@ -163,6 +164,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'moderator' }); + await addRoomModerator(this.userId, findResult.rid, user._id); return API.v1.success(); @@ -181,6 +184,8 @@ API.v1.addRoute( }); const user = await getUserFromParams(this.bodyParams); + + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'owner' }); await addRoomOwner(this.userId, findResult.rid, user._id); @@ -908,6 +913,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'user' }); + await removeRoomModerator(this.userId, findResult.rid, user._id); return API.v1.success(); @@ -927,6 +934,8 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); + await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'user' }); + await removeRoomOwner(this.userId, findResult.rid, user._id); return API.v1.success(); diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 0106b36d33ed9..710a1520c972d 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -193,6 +193,10 @@ callbacks.add( 'federation-matrix-after-room-message-updated', ); +callbacks.add('beforeChangeRoomRole', async (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { + await FederationMatrix.addUserRoleRoomScoped(params.roomId, params.fromUserId, params.userId, params.role); +}, callbacks.priority.HIGH, 'federation-matrix-before-change-room-role'); + export const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 2a8410fe37685..7024d6c99b24b 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -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; + 'beforeChangeRoomRole': (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => void; }; export type Hook = diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts index d5885888dfedc..b997cbac84a8e 100644 --- a/apps/meteor/server/services/federation/utils.ts +++ b/apps/meteor/server/services/federation/utils.ts @@ -15,10 +15,18 @@ export function getFederationVersion(): 'matrix' | 'native' | null { } export function isFederationEnabled(): boolean { + if (getFederationVersion() === 'native') { + return true; + } + return settings.get('Federation_Matrix_enabled'); } export function isFederationReady(): boolean { + if (getFederationVersion() === 'native') { + return true; + } + return settings.get('Federation_Matrix_configuration_status') === 'Valid'; } diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 07926f53a9b1b..b40c022876cf5 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -1,7 +1,7 @@ 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 { saveRoomTopic } from '../../../app/channel-settings/server/functions/saveRoomTopic'; @@ -13,6 +13,12 @@ import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { createDirectMessage } from '../../methods/createDirectMessage'; import { saveRoomName } from '../../../app/channel-settings/server'; +import { addRoomModerator } from '../../methods/addRoomModerator'; +import { addRoomOwner } from '../../methods/addRoomOwner'; +import { addRoomLeader } from '../../methods/addRoomLeader'; +import { removeRoomOwner } from '../../methods/removeRoomOwner'; +import { removeRoomLeader } from '../../methods/removeRoomLeader'; +import { removeRoomModerator } from '../../methods/removeRoomModerator'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; @@ -151,4 +157,47 @@ export class RoomService extends ServiceClassInternal implements IRoomService { } 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 (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/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 191cab6058952..612bf0035b0c8 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -723,4 +723,34 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS await this.homeserverServices.room.setRoomTopic(matrixRoomId, userId, topic); } + + async addUserRoleRoomScoped(rid: string, 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${rid}`); + } + + const matrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); + if (!matrixUserId) { + throw new Error(`No Matrix user ID mapping found for user ${userId}`); + } + + const senderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); + if (!senderMatrixUserId) { + throw new Error(`No Matrix user ID mapping found for user ${senderId}`); + } + + const powerLevel = role === 'owner' ? 100 : role === 'moderator' ? 50 : 0; + + await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel); + } } diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index 4628651dd59a8..887b07e3b2d29 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -1,6 +1,6 @@ import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import { MatrixBridgedRoom, MatrixBridgedUser } from '@rocket.chat/models'; +import { MatrixBridgedRoom, MatrixBridgedUser, Rooms } from '@rocket.chat/models'; import { Room } from '@rocket.chat/core-services'; export function room(emitter: Emitter) { @@ -35,4 +35,30 @@ export function room(emitter: Emitter) { await Room.saveRoomTopic(localRoomId, topic, { _id: localUserId, 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 MatrixBridgedRoom.getLocalRoomId(roomId); + if (!localRoomId) { + throw new Error('mapped room not found'); + } + + const localRoom = await Rooms.findOneById(localRoomId); + if (!localRoom) { + throw new Error('mapped room object not found'); + } + + const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + if (!localUserId) { + throw new Error('mapped user not found'); + } + + const localSenderId = await MatrixBridgedUser.getLocalUserIdByExternalId(senderId); + if (!localSenderId) { + throw new Error('mapped user not found'); + } + + await Room.addUserRoleRoomScoped(localSenderId, localUserId, localRoomId, role); + }); } diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index c653124bf9f9f..40073ea07fa7c 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -26,4 +26,5 @@ export interface IFederationMatrixService { updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; updateRoomName(roomId: string, name: string, sender: string): Promise; updateRoomTopic(roomId: string, topic: string, sender: string): Promise; + addUserRoleRoomScoped(rid: string, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise; } diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 6481fc0a2bed4..b3b79a3e9ceea 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -59,4 +59,5 @@ export interface IRoomService { 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; } From a0b52ab8a3907b1bcec5add1422b659999ed878a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 10:32:57 -0300 Subject: [PATCH 32/99] getting it to build with homeserver code, too noisy to squashing rebuild yarn.lock clone and build homeserver repo indentatioin change setup-node order remove existing homeserver path test /tmp diff job :/ must link use bundle branch fix storybook too fix lint error fix old federation tests fix more similar issues ci: remove deploy-preview for PRs (#36725) test: cleanup page-objects (#36665) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> regression: Prevent in emoji picker route to home page (#36720) fix: unhandled rejection from stale runtime (#36670) fix: prevent unexpected token errors from newer syntax in apps (#36625) chore: Move E2E Encryption startup (#36722) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> fix: not receiving focus properly (#36738) i18n: Incomplete Room Name Validation Message (#35318) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> fix: encryption toggle disabled/reset when toggling Private or Broadcast under E2E defaults (#36714) fix: moment js not loading some locales (#36651) fix: Custom notification sounds on incoming messages (#36703) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> fix: not applying to agents (#36747) fix: enhance inquiry locking mechanism with timeout and error handling (#36749) fix: media playback failing due to expired urls (#36622) test: add saml login tests for user without name and user with channels (#36383) more fixes remove homeserver link again more lint issues another coupe cache homeserver stuff another lint probelem yeah i ran lint locally finally. fix the other dockerfiles --- .github/workflows/ci-code-check.yml | 6 ++ .github/workflows/ci-test-e2e.yml | 6 ++ .github/workflows/ci-test-storybook.yml | 5 ++ .github/workflows/ci-test-unit.yml | 5 ++ .github/workflows/ci.yml | 59 ++++++++++++++++++- .gitignore | 2 + apps/meteor/app/api/server/v1/channels.ts | 9 --- apps/meteor/app/api/server/v1/groups.ts | 9 --- .../server/methods/saveRoomSettings.ts | 5 -- .../client/lib/rooms/roomCoordinator.tsx | 22 ------- .../ee/server/hooks/federation/index.ts | 20 +++++-- apps/meteor/lib/callbacks.ts | 1 - apps/meteor/package.json | 1 + apps/meteor/server/services/room/service.ts | 25 ++++---- ee/apps/account-service/Dockerfile | 3 + ee/apps/authorization-service/Dockerfile | 3 + ee/apps/ddp-streamer/Dockerfile | 3 + ee/apps/federation-service/package.json | 1 + ee/apps/federation-service/src/service.ts | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 3 + ee/apps/presence-service/Dockerfile | 3 + ee/apps/queue-worker/Dockerfile | 3 + ee/apps/stream-hub-service/Dockerfile | 3 + .../federation-matrix/src/FederationMatrix.ts | 28 ++++++--- .../src/api/_matrix/invite.ts | 10 ++-- .../src/api/_matrix/profiles.ts | 3 + .../src/api/_matrix/send-join.ts | 3 + .../federation-matrix/src/events/room.ts | 18 +++--- .../src/types/IFederationMatrixService.ts | 1 + .../core-services/src/types/IRoomService.ts | 2 +- yarn.lock | 2 + 31 files changed, 177 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 9c903dcf5a4f7..4cf663465982b 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -31,6 +31,12 @@ jobs: swap-size-gb: 4 - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 84ebeca360068..e06ba921c18e2 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -120,6 +120,12 @@ jobs: mongodb-replica-set: rs0 - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index c5f28d1bdea10..7f293b0fed384 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -25,6 +25,11 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node with: diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 8cfaeef15d192..340b580e4eb4e 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -35,6 +35,11 @@ jobs: job_summary: true comment_on_pr: false - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 569cdece7dddd..d46718ad5abc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,9 +133,43 @@ jobs: "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5.0\", \"6.0\", \"7.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update + build-homeserver: + name: 📦 Build Homeserver + needs: [release-versions] + runs-on: ubuntu-24.04 + steps: + - uses: actions/cache@v4 + id: cache-homeserver + name: cache homeserver + with: + path: homeserver + key: ${{ runner.os }}-homeserver-${{ hashFiles('homeserver/package.json') }} + restore-keys: ${{ runner.os }}-homeserver- + - uses: actions/checkout@v4 + if: steps.cache-homeserver.outputs.cache-hit != 'true' + with: + repository: RocketChat/homeserver + path: homeserver + ref: bundle-for-rocket.chat + - uses: oven-sh/setup-bun@v2 + if: steps.cache-homeserver.outputs.cache-hit != 'true' + - run: | + cd homeserver + bun install + bun run build + bun run bundle:sdk + rm -rf .git + cp -r ../homeserver /tmp/homeserver + if: steps.cache-homeserver.outputs.cache-hit != 'true' + + - uses: actions/upload-artifact@v4 + with: + name: homeserver + path: homeserver + packages-build: name: 📦 Build Packages - needs: [release-versions, notify-draft-services] + needs: [release-versions, notify-draft-services, build-homeserver] runs-on: ubuntu-24.04 steps: - name: Github Info @@ -155,6 +189,13 @@ jobs: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver + - name: Setup NodeJS uses: ./.github/actions/setup-node with: @@ -188,7 +229,7 @@ jobs: build: name: 📦 Meteor Build - coverage - needs: [release-versions, packages-build] + needs: [release-versions, packages-build, build-homeserver] runs-on: ubuntu-24.04 steps: @@ -210,6 +251,12 @@ jobs: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver + - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} @@ -294,7 +341,7 @@ jobs: build-gh-docker-coverage: name: 🚢 Build Docker Images for Testing - needs: [build, release-versions, build-matrix-rust-bindings-for-alpine] + needs: [build, release-versions, build-matrix-rust-bindings-for-alpine, build-homeserver] runs-on: ubuntu-24.04 env: @@ -311,6 +358,12 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: homeserver + path: /tmp/homeserver + - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver + # we only build and publish the actual docker images if not a PR from a fork - uses: ./.github/actions/build-docker if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' diff --git a/.gitignore b/.gitignore index 8741b33f36c35..819869a58a86f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ registration.yaml storybook-static development/tempo-data/ + +homeserver \ No newline at end of file diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 2a5a9ba5df7e8..b5574e239ee4b 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -57,7 +57,6 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; -import { callbacks } from '../../../../lib/callbacks'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property async function findChannelByIdOrName({ @@ -563,8 +562,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'moderator' }); - await addRoomModerator(this.userId, findResult._id, user._id); return API.v1.success(); @@ -581,8 +578,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'owner' }); - await addRoomOwner(this.userId, findResult._id, user._id); return API.v1.success(); @@ -1191,8 +1186,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'user' }); - await removeRoomModerator(this.userId, findResult._id, user._id); return API.v1.success(); @@ -1209,8 +1202,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult._id, role: 'user' }); - await removeRoomOwner(this.userId, findResult._id, user._id); return API.v1.success(); diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 784b3ad8d4d76..311e383232171 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -37,7 +37,6 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; -import { callbacks } from '../../../../lib/callbacks'; async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( @@ -164,8 +163,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'moderator' }); - await addRoomModerator(this.userId, findResult.rid, user._id); return API.v1.success(); @@ -184,8 +181,6 @@ API.v1.addRoute( }); const user = await getUserFromParams(this.bodyParams); - - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'owner' }); await addRoomOwner(this.userId, findResult.rid, user._id); @@ -913,8 +908,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'user' }); - await removeRoomModerator(this.userId, findResult.rid, user._id); return API.v1.success(); @@ -934,8 +927,6 @@ API.v1.addRoute( const user = await getUserFromParams(this.bodyParams); - await callbacks.run('beforeChangeRoomRole', { fromUserId: this.userId, userId: user._id, roomId: findResult.rid, role: 'user' }); - await removeRoomOwner(this.userId, findResult.rid, user._id); return API.v1.success(); diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 745779e2230e1..7cbdca852cd6d 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -231,11 +231,6 @@ const settingSavers: RoomSettingsSavers = { await saveRoomTopic(rid, value, user); } }, - async sidepanel({ value, rid, room }) { - if (JSON.stringify(value) !== JSON.stringify(room.sidepanel)) { - await Rooms.setSidepanelById(rid, value); - } - }, async roomAnnouncement({ value, room, rid, user }) { if (!value && !room.announcement) { return; diff --git a/apps/meteor/client/lib/rooms/roomCoordinator.tsx b/apps/meteor/client/lib/rooms/roomCoordinator.tsx index 26b6afb82bc3a..1e99fa0bfa7df 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.tsx +++ b/apps/meteor/client/lib/rooms/roomCoordinator.tsx @@ -140,28 +140,6 @@ class RoomCoordinatorClient extends RoomCoordinator { return false; } - // #ToDo: Move this out of the RoomCoordinator - public archived(rid: string): boolean { - const room = Rooms.findOne({ _id: rid }, { fields: { archived: 1 } }); - return Boolean(room?.archived); - } - - public verifyCanSendMessage(rid: string): boolean { - const room = Rooms.findOne({ _id: rid }, { fields: { t: 1, federated: 1 } }); - if (!room?.t) { - return false; - } - if (!this.getRoomDirectives(room.t).canSendMessage(rid)) { - return false; - } - // TODO: Adjust this to call a central function validator instead of settings - // since there will be more than one setting to check (status, connection, etc.) - if (isRoomFederated(room)) { - return settings.get('Federation_Matrix_enabled') || settings.get('Federation_Service_Enabled'); - } - return true; - } - private validateRoute(route: IRoomTypeRouteConfig): void { const { name, path, link } = route; diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 710a1520c972d..b6a8df3386f58 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -15,6 +15,7 @@ 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 { getFederationVersion } from '../../../../server/services/federation/utils'; // callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); @@ -84,7 +85,12 @@ callbacks.add( callbacks.add( 'federation.onAddUsersToRoom', - async ({ invitees, inviter }, room) => FederationMatrix.inviteUsersToRoom(room, invitees, inviter), + async ({ invitees, inviter }, room) => + FederationMatrix.inviteUsersToRoom( + room, + invitees.map((invitee) => (typeof invitee === 'string' ? invitee : (invitee.username as string))), + inviter, + ), callbacks.priority.MEDIUM, 'native-federation-on-add-users-to-room ', ); @@ -100,7 +106,7 @@ beforeAddUserToRoom.add( await FederationMatrix.inviteUsersToRoom(room, [user.username], inviter); }, callbacks.priority.MEDIUM, - 'native-federation-on-before-add-users-to-room ', + 'native-federation-on-before-add-users-to-room', ); callbacks.add( @@ -193,9 +199,13 @@ callbacks.add( 'federation-matrix-after-room-message-updated', ); -callbacks.add('beforeChangeRoomRole', async (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { - await FederationMatrix.addUserRoleRoomScoped(params.roomId, params.fromUserId, params.userId, params.role); -}, callbacks.priority.HIGH, 'federation-matrix-before-change-room-role'); +beforeChangeRoomRole.add( + async (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { + await FederationMatrix.addUserRoleRoomScoped(params.roomId, params.fromUserId, params.userId, params.role); + }, + callbacks.priority.HIGH, + 'federation-matrix-before-change-room-role', +); export const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 7024d6c99b24b..2a8410fe37685 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -205,7 +205,6 @@ type ChainedCallbackSignatures = { 'roomAvatarChanged': (room: IRoom) => void; 'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise; 'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void; - 'beforeChangeRoomRole': (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => void; }; export type Hook = diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cc470c8759435..ad3dc5acb6712 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -436,6 +436,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", diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index b40c022876cf5..e48eeedecf5c4 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -4,6 +4,7 @@ import { type AtLeast, type IRoom, type IUser, isOmnichannelRoom, isRoomWithJoin 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,14 +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 { createDirectMessage } from '../../methods/createDirectMessage'; -import { saveRoomName } from '../../../app/channel-settings/server'; +import { addRoomLeader } from '../../methods/addRoomLeader'; import { addRoomModerator } from '../../methods/addRoomModerator'; import { addRoomOwner } from '../../methods/addRoomOwner'; -import { addRoomLeader } from '../../methods/addRoomLeader'; -import { removeRoomOwner } from '../../methods/removeRoomOwner'; +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'; @@ -158,32 +158,37 @@ export class RoomService extends ServiceClassInternal implements IRoomService { await saveRoomName(roomId, name, user); } - public async addUserRoleRoomScoped(fromUserId: string, userId: string, roomId: string,role: 'moderator' | 'owner' | 'leader' | 'user'): Promise { + 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 (const currentRole of sub.roles) { + for await (const currentRole of sub.roles) { if (currentRole === 'owner') { await removeRoomOwner(fromUserId, roomId, userId); return; 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/package.json b/ee/apps/federation-service/package.json index ba32bc0a0ab59..aae2942b7dd87 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@types/bun": "latest", "@types/express": "^4.17.17", + "eslint": "~8.45.0", "typescript": "^5.3.0" }, "keywords": [ diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 702b9f53a10d4..1c4ee6a9e0f0f 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -57,7 +57,7 @@ function handleHealthCheck(app: Hono) { app.mount('/_matrix', matrix.getHonoRouter().fetch); app.mount('/.well-known', wellKnown.getHonoRouter().fetch); - + handleHealthCheck(app); serve({ 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/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 612bf0035b0c8..e3ab65012e342 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -723,34 +723,44 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS await this.homeserverServices.room.setRoomTopic(matrixRoomId, userId, topic); } - - async addUserRoleRoomScoped(rid: string, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise { + + async addUserRoleRoomScoped( + rid: string, + 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); if (!matrixRoomId) { throw new Error(`No Matrix room mapping found for room ${rid}`); } - + const matrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); if (!matrixUserId) { throw new Error(`No Matrix user ID mapping found for user ${userId}`); } - + const senderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); if (!senderMatrixUserId) { throw new Error(`No Matrix user ID mapping found for user ${senderId}`); } - - const powerLevel = role === 'owner' ? 100 : role === 'moderator' ? 50 : 0; - + + let powerLevel = 0; + if (role === 'owner') { + powerLevel = 100; + } else if (role === 'moderator') { + powerLevel = 50; + } + await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel); } } diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 7b525c8ade935..263f87c807c60 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -135,7 +135,7 @@ async function runWithBackoff(fn: () => Promise, delaySec = 5) { try { await fn(); } catch (e) { - const delay = delaySec === 625 ? 625 : delaySec ** 2; + const delay = delaySec === 625 ? 625 : delaySec ** 2; console.log(`error occurred, retrying in ${delay}ms`, e); setTimeout(() => { runWithBackoff(fn, delay * 1000); @@ -239,7 +239,7 @@ async function joinRoom({ options: { federatedRoomId: inviteEvent.roomId, creator: senderUserId, - } + }, }); internalRoomId = ourRoom._id; @@ -275,7 +275,7 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { }, async (c) => { const { roomId, eventId } = c.req.param(); - const { event, room_version } = await c.req.json(); + const { event, room_version: roomVersion } = await c.req.json(); const userToCheck = event.state_key as string; @@ -283,7 +283,7 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { throw new Error('join event has missing state key, unable to determine user to join'); } - const [username, _domain] = userToCheck.split(':'); + const [username /* domain */] = userToCheck.split(':'); // TODO: check domain @@ -293,7 +293,7 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { throw new Error('user not found not processing invite'); } - const inviteEvent = await invite.processInvite(event, roomId, eventId, room_version); + const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); void startJoiningRoom({ inviteEvent, diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 4ca8e6646663c..60db4e87150e8 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -144,6 +144,7 @@ const MakeJoinParamsSchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isMakeJoinParamsProps = ajv.compile(MakeJoinParamsSchema); const MakeJoinQuerySchema = { @@ -161,6 +162,7 @@ const MakeJoinQuerySchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isMakeJoinQueryProps = ajv.compile(MakeJoinQuerySchema); const MakeJoinResponseSchema = { @@ -246,6 +248,7 @@ const MakeJoinResponseSchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isMakeJoinResponseProps = ajv.compile(MakeJoinResponseSchema); const GetMissingEventsParamsSchema = { diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index 3a75458ca74db..b2c0008ee420c 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -47,6 +47,7 @@ const SendJoinParamsSchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema); const EventHashSchema = { @@ -183,6 +184,7 @@ const SendJoinEventSchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isSendJoinEventProps = ajv.compile(SendJoinEventSchema); const SendJoinResponseSchema = { @@ -216,6 +218,7 @@ const SendJoinResponseSchema = { }; // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema); export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index 887b07e3b2d29..a083485f99751 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -1,12 +1,12 @@ -import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import { MatrixBridgedRoom, MatrixBridgedUser, Rooms } from '@rocket.chat/models'; import { Room } from '@rocket.chat/core-services'; +import type { Emitter } from '@rocket.chat/emitter'; +import { MatrixBridgedRoom, MatrixBridgedUser, Rooms } 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 MatrixBridgedRoom.getLocalRoomId(roomId); if (!localRoomId) { throw new Error('mapped room not found'); @@ -19,10 +19,10 @@ export function room(emitter: Emitter) { await Room.saveRoomName(localRoomId, localUserId, name); }); - + emitter.on('homeserver.matrix.room.topic', async (data) => { const { room_id: roomId, topic, user_id: userId } = data; - + const localRoomId = await MatrixBridgedRoom.getLocalRoomId(roomId); if (!localRoomId) { throw new Error('mapped room not found'); @@ -35,15 +35,15 @@ export function room(emitter: Emitter) { await Room.saveRoomTopic(localRoomId, topic, { _id: localUserId, 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 MatrixBridgedRoom.getLocalRoomId(roomId); if (!localRoomId) { throw new Error('mapped room not found'); } - + const localRoom = await Rooms.findOneById(localRoomId); if (!localRoom) { throw new Error('mapped room object not found'); @@ -58,7 +58,7 @@ export function room(emitter: Emitter) { if (!localSenderId) { throw new Error('mapped user not found'); } - + await Room.addUserRoleRoomScoped(localSenderId, localUserId, localRoomId, role); }); } diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 40073ea07fa7c..9d7e343115b43 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -27,4 +27,5 @@ export interface IFederationMatrixService { updateRoomName(roomId: string, name: string, sender: string): Promise; updateRoomTopic(roomId: string, topic: string, sender: string): Promise; addUserRoleRoomScoped(rid: string, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise; + inviteUsersToRoom(room: IRoom, usersUserName: string[], inviter: Pick): Promise; } diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index b3b79a3e9ceea..0bb6ae3e6a0d8 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -59,5 +59,5 @@ export interface IRoomService { 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; + addUserRoleRoomScoped(fromUserId: string, userId: string, roomId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise; } diff --git a/yarn.lock b/yarn.lock index a1b6bd3a7f2c2..3413fb688807e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7774,6 +7774,7 @@ __metadata: "@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:^8.16.0" polka: "npm:^0.5.2" @@ -8753,6 +8754,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" From d3f8559906a1503e01635ceb3c23f54cd316dbfd Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Thu, 21 Aug 2025 16:22:52 +0530 Subject: [PATCH 33/99] fix: ui issue and simplify config passing to homeserver (#36761) --- .../server/settings/federation-service.ts | 7 ------- .../federation-matrix/src/FederationMatrix.ts | 17 +++++++++++++---- .../src/helpers/domain.builder.ts | 8 ++++---- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 75271da3e2dcc..65174cf6f008d 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -10,13 +10,6 @@ export const createFederationServiceSettings = async (): Promise => { alert: 'Federation_Service_Alert', }); - await this.add('Federation_Service_Matrix_Domain', 'localhost', { - type: 'string', - i18nLabel: 'Federation_Service_Matrix_Domain', - i18nDescription: 'Federation_Service_Matrix_Domain_Description', - public: true, - }); - await this.add('Federation_Service_Matrix_Port', 3000, { type: 'int', i18nLabel: 'Federation_Service_Matrix_Port', diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index e3ab65012e342..85c3870fe87f6 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -47,17 +47,26 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS static async create(emitter?: Emitter): Promise { const instance = new FederationMatrix(emitter); const settingsSigningKey = await Settings.get('Federation_Service_Matrix_Signing_Key'); + + const siteUrl = await Settings.get('Site_Url'); + + const serverHostname = new URL(siteUrl).hostname; + + 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({ - serverName: process.env.MATRIX_SERVER_NAME || 'rc1', + serverName: serverHostname, keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), - matrixDomain: process.env.MATRIX_DOMAIN || 'rc1', + matrixDomain: serverHostname, version: process.env.SERVER_VERSION || '1.0', port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), signingKey: settingsSigningKey, signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', database: { - uri: process.env.MONGODB_URI || 'mongodb://localhost:3001/meteor', - name: process.env.DATABASE_NAME || 'meteor', + uri: mongoUri, + name: dbName, poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10), }, }); diff --git a/ee/packages/federation-matrix/src/helpers/domain.builder.ts b/ee/packages/federation-matrix/src/helpers/domain.builder.ts index 478cc48564c18..cd78fb4c1cf5a 100644 --- a/ee/packages/federation-matrix/src/helpers/domain.builder.ts +++ b/ee/packages/federation-matrix/src/helpers/domain.builder.ts @@ -1,13 +1,13 @@ -import { Settings } from '@rocket.chat/models'; +import { Settings } from '@rocket.chat/core-services'; export const getMatrixLocalDomain = async () => { - const port = await Settings.findOneById('Federation_Service_Matrix_Port'); - const domain = await Settings.findOneById('Federation_Service_Matrix_Domain'); + const port = await Settings.get('Federation_Service_Matrix_Port'); + const domain = await Settings.get('Site_Url'); if (!port || !domain) { throw new Error('Matrix domain or port not found'); } - const matrixDomain = port.value === 443 || port.value === 80 ? domain.value : `${domain.value}:${port.value}`; + const matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; return String(matrixDomain); }; From a05b6a229248a31145b3ee42aff638d921daa3ba Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Thu, 21 Aug 2025 16:32:14 +0530 Subject: [PATCH 34/99] cache homeserver repo correctly --- .github/workflows/ci.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d46718ad5abc3..9d90223a61f78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,19 +138,18 @@ jobs: needs: [release-versions] runs-on: ubuntu-24.04 steps: - - uses: actions/cache@v4 - id: cache-homeserver - name: cache homeserver - with: - path: homeserver - key: ${{ runner.os }}-homeserver-${{ hashFiles('homeserver/package.json') }} - restore-keys: ${{ runner.os }}-homeserver- - uses: actions/checkout@v4 - if: steps.cache-homeserver.outputs.cache-hit != 'true' with: repository: RocketChat/homeserver path: homeserver - ref: bundle-for-rocket.chat + + - uses: actions/cache@v4 + id: cache-homeserver + name: cache homeserver + with: + path: /tmp/homeserver + key: homeserver-${{ hashFiles('homeserver/.git/refs/heads/main') }} + - uses: oven-sh/setup-bun@v2 if: steps.cache-homeserver.outputs.cache-hit != 'true' - run: | @@ -165,7 +164,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: homeserver - path: homeserver + path: /tmp/homeserver packages-build: name: 📦 Build Packages From 222f01ced0aa071e4766d309d8168603c35d28e5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 21 Aug 2025 20:21:46 -0300 Subject: [PATCH 35/99] fix typecheck --- .../content/attachments/file/hooks/useReloadOnError.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(); From 6da5f2da76dbebb7cc0f4d6697e41921e15bb494 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 1 Sep 2025 13:41:21 -0300 Subject: [PATCH 36/99] refactor(federation-matrix): remove the need of `getMatrixLocalDomain` (#36839) --- .../federation-matrix/src/FederationMatrix.ts | 67 +++++++------------ .../federation-matrix/src/events/index.ts | 4 +- .../federation-matrix/src/events/message.ts | 12 ++-- .../src/helpers/domain.builder.ts | 13 ---- 4 files changed, 32 insertions(+), 64 deletions(-) delete mode 100644 ee/packages/federation-matrix/src/helpers/domain.builder.ts diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 85c3870fe87f6..64f0b6726dd5e 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -22,7 +22,6 @@ import { getMatrixSendJoinRoutes } from './api/_matrix/send-join'; import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; import { getFederationVersionsRoutes } from './api/_matrix/versions'; import { registerEvents } from './events'; -import { getMatrixLocalDomain } from './helpers/domain.builder'; import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; @@ -33,7 +32,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS private homeserverServices: HomeserverServices; - private matrixDomain: string; + private serverName: string; private readonly logger = new Logger(this.name); @@ -52,6 +51,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const serverHostname = new URL(siteUrl).hostname; + instance.serverName = serverHostname; + const mongoUri = process.env.MONGO_URL || 'mongodb://localhost:3001/meteor'; const dbName = process.env.DATABASE_NAME || new URL(mongoUri).pathname.slice(1); @@ -154,22 +155,12 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS async created(): Promise { try { - registerEvents(this.eventHandler); + registerEvents(this.eventHandler, this.serverName); } catch (error) { this.logger.warn('Homeserver module not available, running in limited mode'); } } - async getMatrixDomain(): Promise { - if (this.matrixDomain) { - return this.matrixDomain; - } - - this.matrixDomain = await getMatrixLocalDomain(); - - return this.matrixDomain; - } - getAllRoutes() { return this.httpRoutes; } @@ -185,8 +176,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } try { - const matrixDomain = await this.getMatrixDomain(); - const matrixUserId = `@${owner.username}:${matrixDomain}`; + const matrixUserId = `@${owner.username}:${this.serverName}`; const roomName = room.name || room.fname || 'Untitled Room'; // canonical alias computed from name @@ -194,7 +184,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug('Matrix room created:', matrixRoomResult); - await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, matrixDomain); + await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, this.serverName); await saveExternalUserIdForLocalUser(owner, matrixUserId); @@ -207,7 +197,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS // TODO: Check if it is external user - split domain etc const localUserId = await Users.findOneByUsername(member); if (localUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, this.serverName); // continue; } } catch (error) { @@ -221,7 +211,6 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug('Room creation completed successfully', room._id); } catch (error) { - console.log(error); this.logger.error('Failed to create room:', error); throw error; } @@ -234,11 +223,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw new Error(`No Matrix room mapping found for room ${room._id}`); } - const matrixDomain = await this.getMatrixDomain(); - const matrixUserId = `@${user.username}:${matrixDomain}`; + const matrixUserId = `@${user.username}:${this.serverName}`; const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); if (!existingMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, this.serverName); } if (!this.homeserverServices) { @@ -253,11 +241,11 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const parsedMessage = await toExternalMessageFormat({ message: message.msg, externalRoomId: matrixRoomId, - homeServerDomain: await this.getMatrixDomain(), + homeServerDomain: await this.serverName, }); if (!message.tmid) { if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); if (!quoteMessage) { throw new Error('Failed to retrieve quote message'); } @@ -287,7 +275,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const latestThreadEventId = latestThreadMessage?.federation?.eventId; if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); if (!quoteMessage) { throw new Error('Failed to retrieve quote message'); } @@ -312,7 +300,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } else { this.logger.warn('Thread root event ID not found, sending as regular message'); if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, matrixDomain); + const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); if (!quoteMessage) { throw new Error('Failed to retrieve quote message'); } @@ -391,11 +379,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS if (!matrixRoomId) { throw new Error(`No Matrix room mapping found for room ${message.rid}`); } - const matrixDomain = await this.getMatrixDomain(); - const matrixUserId = `@${message.u.username}:${matrixDomain}`; + const matrixUserId = `@${message.u.username}:${this.serverName}`; const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(message.u._id); if (!existingMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(message.u._id, matrixUserId, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(message.u._id, matrixUserId, true, this.serverName); } if (!this.homeserverServices) { @@ -422,8 +409,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw new Error(`No Matrix room mapping found for room ${room._id}`); } - const matrixDomain = await this.getMatrixDomain(); - const inviterUserId = `@${inviter.username}:${matrixDomain}`; + const inviterUserId = `@${inviter.username}:${this.serverName}`; await Promise.all( usersUserName.map(async (username) => { @@ -443,8 +429,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS await Room.addUserToRoom(room._id, localUser, { _id: inviter._id, username: inviter.username }); let externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); if (!externalUserId) { - externalUserId = `@${username}:${matrixDomain}`; - await MatrixBridgedUser.createOrUpdateByLocalId(localUser._id, externalUserId, false, matrixDomain); + externalUserId = `@${username}:${this.serverName}`; + await MatrixBridgedUser.createOrUpdateByLocalId(localUser._id, externalUserId, false, this.serverName); } await this.homeserverServices.invite.inviteUserToRoom(externalUserId, matrixRoomId, inviterUserId); } @@ -577,13 +563,12 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } - const matrixDomain = await this.getMatrixDomain(); - const matrixUserId = `@${user.username}:${matrixDomain}`; + const matrixUserId = `@${user.username}:${this.serverName}`; const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); if (!existingMatrixUserId) { // User might not have been bridged yet if they never sent a message - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, this.serverName); } if (!this.homeserverServices) { @@ -616,19 +601,17 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } - const matrixDomain = await this.getMatrixDomain(); - - const kickedMatrixUserId = `@${removedUser.username}:${matrixDomain}`; + const kickedMatrixUserId = `@${removedUser.username}:${this.serverName}`; const existingKickedMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(removedUser._id); if (!existingKickedMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, this.serverName); } const actualKickedMatrixUserId = existingKickedMatrixUserId || kickedMatrixUserId; - const senderMatrixUserId = `@${userWhoRemoved.username}:${matrixDomain}`; + const senderMatrixUserId = `@${userWhoRemoved.username}:${this.serverName}`; const existingSenderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userWhoRemoved._id); if (!existingSenderMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, matrixDomain); + await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, this.serverName); } const actualSenderMatrixUserId = existingSenderMatrixUserId || senderMatrixUserId; @@ -677,7 +660,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const parsedMessage = await toExternalMessageFormat({ message: newContent, externalRoomId: matrixRoomId, - homeServerDomain: await this.getMatrixDomain(), + homeServerDomain: await this.serverName, }); const eventId = await this.homeserverServices.message.updateMessage( matrixRoomId, diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index 4ba1d5673cac1..aa3e1eea17ca3 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -9,9 +9,9 @@ import { ping } from './ping'; import { reaction } from './reaction'; import { room } from './room'; -export function registerEvents(emitter: Emitter) { +export function registerEvents(emitter: Emitter, serverName: string) { ping(emitter); - message(emitter); + message(emitter, serverName); invite(emitter); reaction(emitter); member(emitter); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index a336cfd1253a2..758b727226059 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -6,12 +6,11 @@ import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; -import { getMatrixLocalDomain } from '../helpers/domain.builder'; import { toInternalMessageFormat, toInternalQuoteMessageFormat } from '../helpers/message.parsers'; const logger = new Logger('federation-matrix:message'); -export function message(emitter: Emitter) { +export function message(emitter: Emitter, serverName: string) { emitter.on('homeserver.matrix.message', async (data) => { try { const message = data.content?.body?.toString(); @@ -123,7 +122,6 @@ export function message(emitter: Emitter) { } } - const localDomain = await getMatrixLocalDomain(); const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { logger.debug('Received edited message from Matrix, updating existing message'); @@ -151,7 +149,7 @@ export function message(emitter: Emitter) { messageToReplyToUrl, formattedMessage: data.content.formatted_body || '', rawMessage: message, - homeServerDomain: localDomain, + homeServerDomain: serverName, senderExternalId: data.sender, }); await Message.updateMessage( @@ -168,7 +166,7 @@ export function message(emitter: Emitter) { const formatted = toInternalMessageFormat({ rawMessage: data.content['m.new_content'].body, formattedMessage: data.content.formatted_body || '', - homeServerDomain: localDomain, + homeServerDomain: serverName, senderExternalId: data.sender, }); await Message.updateMessage( @@ -192,7 +190,7 @@ export function message(emitter: Emitter) { messageToReplyToUrl, formattedMessage: data.content.formatted_body || '', rawMessage: message, - homeServerDomain: localDomain, + homeServerDomain: serverName, senderExternalId: data.sender, }); await Message.saveMessageFromFederation({ @@ -208,7 +206,7 @@ export function message(emitter: Emitter) { const formatted = await toInternalMessageFormat({ rawMessage: message, formattedMessage: data.content.formatted_body || '', - homeServerDomain: localDomain, + homeServerDomain: serverName, senderExternalId: data.sender, }); await Message.saveMessageFromFederation({ diff --git a/ee/packages/federation-matrix/src/helpers/domain.builder.ts b/ee/packages/federation-matrix/src/helpers/domain.builder.ts deleted file mode 100644 index cd78fb4c1cf5a..0000000000000 --- a/ee/packages/federation-matrix/src/helpers/domain.builder.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Settings } from '@rocket.chat/core-services'; - -export const getMatrixLocalDomain = async () => { - const port = await Settings.get('Federation_Service_Matrix_Port'); - const domain = await Settings.get('Site_Url'); - if (!port || !domain) { - throw new Error('Matrix domain or port not found'); - } - - const matrixDomain = port === 443 || port === 80 ? domain : `${domain}:${port}`; - - return String(matrixDomain); -}; From 35e5f55aeee2a387fbd5a362f1c4aee55e65daf4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 3 Sep 2025 13:36:23 -0300 Subject: [PATCH 37/99] chore: await createFederationContainer for proper async handling --- ee/packages/federation-matrix/src/FederationMatrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 64f0b6726dd5e..6e7b5836e1986 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -76,7 +76,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS emitter: instance.eventHandler, }; - createFederationContainer(containerOptions, config); + await createFederationContainer(containerOptions, config); instance.homeserverServices = getAllServices(); instance.buildMatrixHTTPRoutes(); instance.onEvent('user.typing', async ({ isTyping, roomId, user: { username } }): Promise => { From c67bff0a943f07687ab683017b722c8510ff09c2 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 3 Sep 2025 16:48:59 -0300 Subject: [PATCH 38/99] fix: use new transaction entrypoint --- .../src/api/_matrix/transactions.ts | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index f6b76acd4a0d5..15b2f7f473fad 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -94,12 +94,21 @@ const EventBaseSchema = { nullable: true, }, }, - required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events', 'origin'], + 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, @@ -117,7 +126,7 @@ const SendTransactionBodySchema = { nullable: true, }, }, - required: ['pdus'], + required: ['origin', 'origin_server_ts', 'pdus'], }; const isSendTransactionBodyProps = ajv.compile(SendTransactionBodySchema); @@ -172,14 +181,24 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { async (c) => { const body = await c.req.json(); - const { pdus = [], edus = [] } = body; - - if (pdus.length > 0) { - await event.processIncomingPDUs(pdus); - } - - if (edus.length > 0) { - await event.processIncomingEDUs(edus); + 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 { From 6d82d92a9d6e1d8050cf1a9df9d53f702a1b93c7 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Wed, 3 Sep 2025 17:06:10 -0300 Subject: [PATCH 39/99] refactor: sets media configs on FederationMatrix init (#36862) --- .../federation-matrix/src/FederationMatrix.ts | 19 +++++++++++++++++++ .../src/api/_matrix/versions.ts | 6 ++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 6e7b5836e1986..0272b7bb81c96 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -70,6 +70,25 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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 = { diff --git a/ee/packages/federation-matrix/src/api/_matrix/versions.ts b/ee/packages/federation-matrix/src/api/_matrix/versions.ts index 7b2dcc280cb9d..cac68d2f26c9e 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/versions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -38,12 +38,10 @@ export const getFederationVersionsRoutes = (services: HomeserverServices) => { license: ['federation'], }, async () => { - const serverConfig = config.getServerConfig(); - const response = { server: { - name: serverConfig.name, - version: serverConfig.version, + name: config.serverName, + version: config.version, }, }; From 14285f30cda721724c04f3df52aa55b137be0132 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 11 Sep 2025 16:42:58 -0300 Subject: [PATCH 40/99] feat: use native federation event queue (#36922) --- apps/meteor/ee/server/startup/federation.ts | 3 ++- ee/apps/federation-service/package.json | 1 + ee/apps/federation-service/src/service.ts | 3 ++- ee/packages/federation-matrix/src/FederationMatrix.ts | 3 ++- yarn.lock | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts index 3a9d8b196cdc4..c98cf7b7c1371 100644 --- a/apps/meteor/ee/server/startup/federation.ts +++ b/apps/meteor/ee/server/startup/federation.ts @@ -1,5 +1,6 @@ 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'; @@ -24,7 +25,7 @@ export const startFederationService = async (): Promise => { } logger.debug('Starting federation-matrix service'); - federationMatrixService = await FederationMatrix.create(); + federationMatrixService = await FederationMatrix.create(InstanceStatus.id()); try { api.registerService(federationMatrixService); diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index aae2942b7dd87..9698622425be5 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -27,6 +27,7 @@ "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:*", "@rocket.chat/network-broker": "workspace:^", diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 1c4ee6a9e0f0f..ce944b838ecc2 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -1,6 +1,7 @@ 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'; @@ -49,7 +50,7 @@ function handleHealthCheck(app: Hono) { } const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); - const federationMatrix = await FederationMatrix.create(); + const federationMatrix = await FederationMatrix.create(InstanceStatus.id()); api.registerService(federationMatrix); const app = new Hono(); diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 0272b7bb81c96..2eecb594823d8 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -43,7 +43,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.eventHandler = emitter || new Emitter(); } - static async create(emitter?: Emitter): Promise { + static async create(instanceId: string, emitter?: Emitter): Promise { const instance = new FederationMatrix(emitter); const settingsSigningKey = await Settings.get('Federation_Service_Matrix_Signing_Key'); @@ -58,6 +58,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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, diff --git a/yarn.lock b/yarn.lock index 3413fb688807e..911d95e5e0cd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7769,6 +7769,7 @@ __metadata: "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" "@rocket.chat/http-router": "workspace:*" + "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:*" "@rocket.chat/network-broker": "workspace:^" From f2f7cd22f143bd239c5712a429b09b591106b4c0 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 12 Sep 2025 14:54:58 -0300 Subject: [PATCH 41/99] feat: adds Federation GET /events/{eventId} endpoint (#36873) --- .../src/api/_matrix/transactions.ts | 155 +++++++++++++----- 1 file changed, 116 insertions(+), 39 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 15b2f7f473fad..d05461ae85b5b 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -15,6 +15,45 @@ const SendTransactionParamsSchema = { 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: { @@ -164,50 +203,88 @@ const ErrorResponseSchema = { const isErrorResponseProps = ajv.compile(ErrorResponseSchema); export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { - const { event } = services; - - 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(); + const { event, config } = 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: {}, + }; + } - 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', + pdus: {}, + edus: {}, }, + statusCode: 200, }; - } - - return { - statusCode: 400, - body: {}, - }; - } - - return { - body: { - pdus: {}, - edus: {}, }, - statusCode: 200, - }; - }, + ) + + // GET /_matrix/federation/v1/event/{eventId} + .get( + '/v1/event/:eventId', + { + params: isGetEventParamsProps, + response: { + 200: isGetEventResponseProps, + }, + tags: ['Federation'], + license: ['federation'], + }, + async (c) => { + const eventData = await event.getEventById(c.req.param('eventId')); + if (!eventData) { + return { + body: { + errcode: 'M_NOT_FOUND', + error: 'Event not found', + }, + statusCode: 404, + }; + } + + return { + body: { + origin_server_ts: new Date().getTime(), + origin: config.serverName, + pdus: [eventData.event], + }, + statusCode: 200, + }; + }, + ) ); }; From b7c35e3466d9fc34ec125b084bf11b7a32941f32 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 15 Sep 2025 16:01:34 -0300 Subject: [PATCH 42/99] feat: adds get state and state_id endpoints (#36929) --- .../src/api/_matrix/transactions.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index d05461ae85b5b..8c4a244e740ac 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -202,6 +202,54 @@ const ErrorResponseSchema = { 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, config } = services; @@ -253,6 +301,66 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { }, ) + // 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); + + 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); + return { + statusCode: 200, + body: state, + }; + }, + ) // GET /_matrix/federation/v1/event/{eventId} .get( '/v1/event/:eventId', From 2deb74f18b2c8e5fd28a2e408e02cc5e3b3da0c4 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 16 Sep 2025 12:25:37 -0300 Subject: [PATCH 43/99] feat: DMs (#36762) Co-authored-by: Ricardo Garim Co-authored-by: Marcos Defendi Co-authored-by: Guilherme Gazzo Co-authored-by: Debdut Chakraborty Co-authored-by: Debdut Chakraborty Co-authored-by: Rodrigo Nascimento --- .../lib/server/functions/createDirectRoom.ts | 2 +- .../ee/server/hooks/federation/index.ts | 22 +++ .../federation-matrix/src/FederationMatrix.ts | 160 +++++++++++++++++- .../src/api/_matrix/invite.ts | 118 +++++++++++-- .../federation-matrix/src/events/invite.ts | 2 +- .../src/types/IFederationMatrixService.ts | 2 + 6 files changed, 289 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index f77ee1f55901b..5ebe3b0f639b4 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -64,7 +64,7 @@ export async function createDirectRoom( const membersUsernames: string[] = members .map((member) => { if (typeof member === 'string') { - return member.replace('@', ''); + return member; } return member.username; }) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index b6a8df3386f58..d23a30075d49d 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -207,6 +207,28 @@ beforeChangeRoomRole.add( 'federation-matrix-before-change-room-role', ); +callbacks.add( + 'beforeCreateDirectRoom', + async (members: IUser[] | string[]): Promise => { + 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'] }): Promise => { + if (!room || !params || !params.creatorId || !params.creatorId) { + return; + } + + await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId); + }, + callbacks.priority.HIGH, + 'federation-matrix-after-create-direct-room', +); + export const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 2eecb594823d8..6767e1d2a0e34 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -231,11 +231,164 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug('Room creation completed successfully', room._id); } catch (error) { + console.log(error); this.logger.error('Failed to create room:', error); throw error; } } + async ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise { + try { + this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length }); + + for await (const member of members) { + let username: string; + + if (typeof member === 'string') { + username = member; + } else { + username = member.username as string; + } + + if (!username.includes(':') && !username.includes('@')) { + continue; + } + + const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`; + + const existingUser = await Users.findOneByUsername(username); + if (existingUser) { + const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id); + if (!existingBridge) { + const remoteDomain = externalUserId.split(':')[1] || this.serverName; + await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain); + } + continue; + } + + this.logger.debug('Creating federated user locally', { externalUserId, username }); + + const remoteDomain = externalUserId.split(':')[1] || this.serverName; + const localName = username.split(':')[0]?.replace('@', '') || username; + + const newUser = { + username, + name: localName, + type: 'user' as const, + status: UserStatus.OFFLINE, + active: true, + roles: ['user'], + requirePasswordChange: false, + federated: true, + createdAt: new Date(), + _updatedAt: new Date(), + }; + + const { insertedId } = await Users.insertOne(newUser); + await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, remoteDomain); + + this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId }); + } + } 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 matrixUserId = `@${creator.username}:${this.serverName}`; + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(creator._id); + if (!existingMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(creator._id, matrixUserId, true, this.serverName); + } + + const actualMatrixUserId = existingMatrixUserId || matrixUserId; + + 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'); + } + + 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 { + memberMatrixUserId = `@${member.username}:${this.serverName}`; + memberId = member._id; + } + + if (memberId) { + const existingMemberMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(memberId); + + if (!existingMemberMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(memberId, memberMatrixUserId, true, this.serverName); + } + } + + 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); + 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; + } + } + async sendMessage(message: IMessage, room: IRoom, user: IUser): Promise { try { const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); @@ -261,7 +414,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const parsedMessage = await toExternalMessageFormat({ message: message.msg, externalRoomId: matrixRoomId, - homeServerDomain: await this.serverName, + homeServerDomain: this.serverName, }); if (!message.tmid) { if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { @@ -371,7 +524,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } const messageToReplyTo = await Messages.findOneById(messageToReplyToId); - if (!messageToReplyTo || !messageToReplyTo.federation?.eventId) { + if (!messageToReplyTo?.federation?.eventId) { return; } @@ -680,7 +833,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const parsedMessage = await toExternalMessageFormat({ message: newContent, externalRoomId: matrixRoomId, - homeServerDomain: await this.serverName, + homeServerDomain: this.serverName, }); const eventId = await this.homeserverServices.message.updateMessage( matrixRoomId, @@ -772,7 +925,6 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } else if (role === 'moderator') { powerLevel = 50; } - await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel); } } diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 263f87c807c60..7175e9936ad27 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -143,6 +143,45 @@ async function runWithBackoff(fn: () => Promise, delaySec = 5) { } } +async function isDirectMessage(matrixRoom: unknown, inviteEvent: PersistentEventBase): Promise { + try { + const room = matrixRoom as { getEvent?: (type: string) => { content?: { is_direct?: boolean } } }; + const creationEvent = room.getEvent?.('m.room.create'); + if (creationEvent?.content?.is_direct) { + return true; + } + + const roomWithEvents = matrixRoom as { getEvents?: (type: string) => Array<{ content?: { membership?: string }; stateKey?: string }> }; + const memberEvents = roomWithEvents.getEvents?.('m.room.member'); + const joinedMembers = + memberEvents?.filter((event) => event.content?.membership === 'join' || event.content?.membership === 'invite') || []; + + if (joinedMembers.length === 2) { + return true; + } + + const uniqueUsers = new Set(); + if (memberEvents) { + for (const event of memberEvents) { + if (event.content?.membership === 'join' || event.content?.membership === 'invite') { + if (event.stateKey) { + uniqueUsers.add(event.stateKey); + } + } + } + } + uniqueUsers.add(inviteEvent.sender); + if (inviteEvent.stateKey) { + uniqueUsers.add(inviteEvent.stateKey); + } + + return uniqueUsers.size <= 2; + } catch (error) { + console.log('Could not determine if room is DM:', error); + return false; + } +} + async function joinRoom({ inviteEvent, user, // ours trying to join the room @@ -168,9 +207,11 @@ async function joinRoom({ throw new Error('room not found not processing invite'); } - // we only understand these two types of rooms - if (!matrixRoom.isPublic() && !matrixRoom.isInviteOnly()) { - throw new Error('room is neither public not private, rocketchat is unable to join for now'); + // we only understand these two types of rooms, plus direct messages + const isDM = await isDirectMessage(matrixRoom, inviteEvent); + + 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 @@ -233,14 +274,65 @@ async function joinRoom({ const internalMappedRoomId = await MatrixBridgedRoom.getLocalRoomId(inviteEvent.roomId); if (!internalMappedRoomId) { - const ourRoom = await Room.create(senderUserId, { - type: matrixRoom.isPublic() ? 'c' : 'p', - name: matrixRoom.name, - options: { - federatedRoomId: inviteEvent.roomId, - creator: senderUserId, - }, - }); + let roomName: string; + try { + roomName = matrixRoom.name || ''; + } catch (error) { + roomName = inviteEvent.roomId.split(':')[0].replace('!', '') || 'Unnamed Room'; + } + + 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, inviteeUser] = await Promise.all([ + Users.findOneById(senderUserId, { projection: { _id: 1, username: 1 } }), + Promise.resolve(user), + ]); + + if (!senderUser?.username) { + throw new Error('Sender user not found'); + } + if (!inviteeUser?.username) { + throw new Error('inviteeUser user not found'); + } + + ourRoom = await Room.create(senderUserId, { + type: roomType, + name: roomName, + members: [senderUser.username, inviteeUser.username], + options: { + federatedRoomId: inviteEvent.roomId, + creator: senderUserId, + }, + extraData: { + federated: true, + }, + }); + } else { + ourRoom = await Room.create(senderUserId, { + type: roomType, + name: roomName, + options: { + federatedRoomId: inviteEvent.roomId, + creator: senderUserId, + }, + extraData: { + federated: true, + }, + }); + } internalRoomId = ourRoom._id; } else { @@ -253,6 +345,10 @@ async function joinRoom({ } await Room.addUserToRoom(internalRoomId, { _id: user._id }, { _id: senderUserId, username: inviteEvent.sender }); + + if (isDM) { + await MatrixBridgedRoom.createOrUpdateByLocalRoomId(internalRoomId, inviteEvent.roomId, matrixRoom.origin); + } } async function startJoiningRoom(...opts: Parameters) { diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index 314ab87162482..12a40d10e03a3 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -32,7 +32,7 @@ export function invite(emitter: Emitter) { federated: true, }); const serverName = data.sender.split(':')[1] || 'unknown'; - const bridgedUser = await MatrixBridgedUser.findOne({ mri: data.sender }); + const bridgedUser = await MatrixBridgedUser.findOne({ mui: data.sender }); if (!bridgedUser) { await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, data.sender, true, serverName); diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 9d7e343115b43..5140e7a096e65 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -16,6 +16,8 @@ export interface IFederationMatrixService { wellKnown: Router<'/.well-known'>; }; createRoom(room: IRoom, owner: IUser, members: string[]): Promise; + ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise; + createDirectMessageRoom(room: IRoom, members: IUser[], creatorId: IUser['_id']): Promise; sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; deleteMessage(message: IMessage): Promise; sendReaction(messageId: string, reaction: string, user: IUser): Promise; From d2198bbe036a4d950f13af44a4ce7f9cd7d675b1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 16 Sep 2025 17:37:51 -0300 Subject: [PATCH 44/99] chore: refactor federation to use eventID branded types (#36954) --- ee/packages/federation-matrix/src/FederationMatrix.ts | 6 +++--- ee/packages/federation-matrix/src/api/_matrix/send-join.ts | 3 ++- .../federation-matrix/src/api/_matrix/transactions.ts | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 6767e1d2a0e34..b7a2fd1ba2945 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -566,7 +566,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS if (!matrixEventId) { throw new Error(`No Matrix event ID mapping found for message ${message._id}`); } - const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId, matrixUserId); + const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId as EventID, matrixUserId); this.logger.debug('Message Redaction sent to Matrix successfully:', eventId); } catch (error) { @@ -690,7 +690,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const redactionEventId = await this.homeserverServices.message.unsetReaction( matrixRoomId, - eventId, + eventId as EventID, reactionKey, existingMatrixUserId, ); @@ -708,7 +708,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - async getEventById(eventId: string): Promise { + async getEventById(eventId: EventID): Promise { if (!this.homeserverServices) { this.logger.warn('Homeserver services not available'); return null; diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index b2c0008ee420c..2b5b5aa0fdc28 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -1,4 +1,5 @@ import type { HomeserverServices } from '@hs/federation-sdk'; +import type { EventID } from '@hs/room'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -239,7 +240,7 @@ export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { const { roomId, stateKey } = c.req.param(); const body = await c.req.json(); - const response = await sendJoin.sendJoin(roomId, stateKey, body); + const response = await sendJoin.sendJoin(roomId, stateKey as EventID, body); return { body: response, diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 8c4a244e740ac..27a58a054b8dc 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -1,4 +1,5 @@ import type { HomeserverServices } from '@hs/federation-sdk'; +import type { EventID } from '@hs/room'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -325,7 +326,7 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { }; } - const stateIds = await event.getStateIds(roomId, eventId); + const stateIds = await event.getStateIds(roomId, eventId as EventID); return { body: stateIds, @@ -354,7 +355,7 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { statusCode: 404, }; } - const state = await event.getState(roomId, eventId); + const state = await event.getState(roomId, eventId as EventID); return { statusCode: 200, body: state, @@ -373,7 +374,7 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { license: ['federation'], }, async (c) => { - const eventData = await event.getEventById(c.req.param('eventId')); + const eventData = await event.getEventById(c.req.param('eventId') as EventID); if (!eventData) { return { body: { From 27df54a865be00267229c7671167205a805e32bb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 17 Sep 2025 23:06:30 -0300 Subject: [PATCH 45/99] refactor(callbacks): update add method to return a cleanup function --- apps/meteor/lib/callbacks/callbacksBase.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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); + }; } /** From 3c0064b9796fbc363178dc088d6d761309fb3115 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 12:11:01 -0300 Subject: [PATCH 46/99] feat: introduce branded types for IRoomFederated and improve type safety --- .../ee/server/hooks/federation/index.ts | 38 +++++++++++++------ apps/meteor/lib/callbacks.ts | 2 +- .../src/types/IFederationMatrixService.ts | 25 +++++++----- packages/core-typings/src/IRoom.ts | 8 +++- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index d23a30075d49d..299ffab3ff8da 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -22,6 +22,9 @@ import { getFederationVersion } from '../../../../server/services/federation/uti // TODO: move this to the hooks folder callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members, options }) => { + if (!isRoomFederated(room)) { + return; + } const federationVersion = getFederationVersion(); if (federationVersion === 'native') { const federatedRoomId = options?.federatedRoomId; @@ -45,6 +48,10 @@ callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, origi callbacks.add( 'afterSaveMessage', async (message, { room, user }) => { + if (!isRoomFederated(room)) { + return; + } + const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); const federationVersion = getFederationVersion(); @@ -85,12 +92,16 @@ callbacks.add( callbacks.add( 'federation.onAddUsersToRoom', - async ({ invitees, inviter }, room) => - FederationMatrix.inviteUsersToRoom( + async ({ invitees, inviter }, room) => { + if (!isRoomFederated(room)) { + return; + } + await FederationMatrix.inviteUsersToRoom( room, invitees.map((invitee) => (typeof invitee === 'string' ? invitee : (invitee.username as string))), inviter, - ), + ); + }, callbacks.priority.MEDIUM, 'native-federation-on-add-users-to-room ', ); @@ -137,7 +148,7 @@ callbacks.add( afterLeaveRoomCallback.add( async (user: IUser, room: IRoom): Promise => { - if (!room.federated) { + if (!isRoomFederated(room)) { return; } @@ -149,7 +160,7 @@ afterLeaveRoomCallback.add( afterRemoveFromRoomCallback.add( async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise => { - if (!room.federated) { + if (!isRoomFederated(room)) { return; } @@ -172,10 +183,11 @@ callbacks.add( callbacks.add( 'afterRoomTopicChange', - async ({ room, name, userId }) => { - if (name && isRoomFederated(room)) { - await FederationMatrix.updateRoomTopic(room._id, name, userId); + async (_, { room, topic, userId }) => { + if (!isRoomFederated(room)) { + return; } + await FederationMatrix.updateRoomTopic(room._id, topic, userId); }, callbacks.priority.HIGH, 'federation-matrix-after-room-topic-changed', @@ -200,8 +212,11 @@ callbacks.add( ); beforeChangeRoomRole.add( - async (params: { fromUserId: string; userId: string; roomId: string; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { - await FederationMatrix.addUserRoleRoomScoped(params.roomId, params.fromUserId, params.userId, params.role); + async (params: { fromUserId: string; userId: string; room: IRoom; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { + if (!isRoomFederated(params.room)) { + return; + } + await FederationMatrix.addUserRoleRoomScoped(params.room._id, params.fromUserId, params.userId, params.role); }, callbacks.priority.HIGH, 'federation-matrix-before-change-room-role', @@ -219,10 +234,9 @@ callbacks.add( callbacks.add( 'afterCreateDirectRoom', async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }): Promise => { - if (!room || !params || !params.creatorId || !params.creatorId) { + if (!isRoomFederated(room)) { return; } - await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId); }, callbacks.priority.HIGH, diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 2a8410fe37685..f1f8012293542 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -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, userId }: { room: IRoom; topic: string; userId: IUser['_id'] }) => void; }; export type Hook = @@ -212,7 +213,6 @@ export type Hook = | keyof ChainedCallbackSignatures | 'afterProcessOAuthUser' | 'afterRoomArchived' - | 'afterRoomTopicChange' | 'afterSaveUser' | 'afterValidateNewOAuthUser' | 'beforeActivateUser' diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 5140e7a096e65..a1ea231df3535 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,4 +1,4 @@ -import type { AtLeast, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IRoomFederated, IUser } from '@rocket.chat/core-typings'; import type { Router } from '@rocket.chat/http-router'; export interface IRouteContext { @@ -15,19 +15,24 @@ export interface IFederationMatrixService { matrix: Router<'/_matrix'>; wellKnown: Router<'/.well-known'>; }; - createRoom(room: IRoom, owner: IUser, members: string[]): Promise; + createRoom(room: IRoomFederated, owner: IUser, members: string[]): Promise; ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise; - createDirectMessageRoom(room: IRoom, members: IUser[], creatorId: IUser['_id']): Promise; - sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; + createDirectMessageRoom(room: IRoomFederated, members: IUser[], creatorId: IUser['_id']): Promise; + sendMessage(message: IMessage, room: IRoomFederated, user: IUser): Promise; deleteMessage(message: IMessage): Promise; sendReaction(messageId: string, reaction: string, user: IUser): Promise; removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; getEventById(eventId: string): Promise; - leaveRoom(roomId: string, user: IUser): Promise; - kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise; + leaveRoom(rid: IRoomFederated['_id'], user: IUser): Promise; + kickUser(rid: IRoomFederated['_id'], removedUser: IUser, userWhoRemoved: IUser): Promise; updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; - updateRoomName(roomId: string, name: string, sender: string): Promise; - updateRoomTopic(roomId: string, topic: string, sender: string): Promise; - addUserRoleRoomScoped(rid: string, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise; - inviteUsersToRoom(room: IRoom, usersUserName: string[], inviter: Pick): Promise; + updateRoomName(rid: IRoomFederated['_id'], name: string, sender: string): Promise; + updateRoomTopic(rid: IRoomFederated['_id'], topic: string, sender: string): Promise; + addUserRoleRoomScoped( + rid: IRoomFederated['_id'], + senderId: string, + userId: string, + role: 'moderator' | 'owner' | 'leader' | 'user', + ): Promise; + inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: Pick): Promise; } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 4019597aefb05..6159362ad9cca 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -102,12 +102,16 @@ 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}`; }; From 9619660ff745095c887cac6f038dcf8981b18b52 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 12:12:10 -0300 Subject: [PATCH 47/99] chore: change afterRoomTopicChange signature --- .../app/channel-settings/server/functions/saveRoomTopic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts index a59f2ba82fba5..947f38e6df444 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, userId: user._id }); return update; }; From 98ef8506c2a6261e27c960bf1cd82cb253a5d530 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 12:16:24 -0300 Subject: [PATCH 48/99] beforeChangeRoomRole signrature --- apps/meteor/lib/callbacks/beforeChangeRoomRole.ts | 4 +++- apps/meteor/server/methods/addRoomModerator.ts | 2 +- apps/meteor/server/methods/addRoomOwner.ts | 2 +- apps/meteor/server/methods/removeRoomModerator.ts | 2 +- apps/meteor/server/methods/removeRoomOwner.ts | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) 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/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index ad9cae2d7a236..6cfec0032d8ce 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -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..f13426a7013f9 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -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/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') || []); From 48d1d0e6a9fa00d96b712b31d6fe52d5338930ce Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 21:03:37 -0300 Subject: [PATCH 49/99] refactor(addUserToRoom): change inviter type --- apps/meteor/app/lib/server/functions/addUserToRoom.ts | 6 +++--- apps/meteor/lib/callbacks/beforeAddUserToRoom.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 3820068d76668..70e0746c18cd5 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -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/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'); From 1a3fb7c777e8ee234c8545c3abbca77a1f401ac8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 21:10:32 -0300 Subject: [PATCH 50/99] REVIEW `setupTypingEventListenerForRoom` --- apps/meteor/ee/server/hooks/federation/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 299ffab3ff8da..72227f2153152 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -243,7 +243,8 @@ callbacks.add( 'federation-matrix-after-create-direct-room', ); -export const setupTypingEventListenerForRoom = (roomId: string): void => { +// TODO: THIS IS NOT READY FOR PRODUCTION! IMPOSSIBLE TO ADD ONE LISTENER PER ROOM! +const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { void api.broadcast('user.typing', { From ded34bcf76c8c75d6f2c95b57ffe7857c6351e6e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 21:18:05 -0300 Subject: [PATCH 51/99] feat: add license middleware for federation support --- ee/packages/federation-matrix/package.json | 1 + ee/packages/federation-matrix/src/FederationMatrix.ts | 2 ++ .../src/api/middlewares/isLicenseEnabled.ts | 9 +++++++++ yarn.lock | 1 + 4 files changed, 13 insertions(+) create mode 100644 ee/packages/federation-matrix/src/api/middlewares/isLicenseEnabled.ts diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index ef820a4a32143..8b1b58155ed15 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -39,6 +39,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/http-router": "workspace:^", + "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index b7a2fd1ba2945..30225a4a6128b 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -21,6 +21,7 @@ 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 { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; import { registerEvents } from './events'; import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; @@ -160,6 +161,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const wellKnown = new Router('/.well-known'); matrix + .use(isLicenseEnabledMiddleware) .use(getMatrixInviteRoutes(this.homeserverServices)) .use(getMatrixProfilesRoutes(this.homeserverServices)) .use(getMatrixRoomsRoutes(this.homeserverServices)) 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/yarn.lock b/yarn.lock index 911d95e5e0cd3..5e6a8ba4e237e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7738,6 +7738,7 @@ __metadata: "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/http-router": "workspace:^" + "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" From 0d89d11b3f04a7f612aa7620ac7d29de362b0936 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 21:21:55 -0300 Subject: [PATCH 52/99] feat: add middleware to check if federation is enabled --- ee/packages/federation-matrix/src/FederationMatrix.ts | 2 ++ .../src/api/middlewares/isFederationEnabled.ts | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 30225a4a6128b..c2591bbab881c 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -21,6 +21,7 @@ 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 { isFederationEnabledMiddleware } from './api/middlewares/isFederationEnabled'; import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; import { registerEvents } from './events'; import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; @@ -161,6 +162,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const wellKnown = new Router('/.well-known'); matrix + .use(isFederationEnabledMiddleware) .use(isLicenseEnabledMiddleware) .use(getMatrixInviteRoutes(this.homeserverServices)) .use(getMatrixProfilesRoutes(this.homeserverServices)) 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..ae60ab600e11b --- /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_Enabled')) { + return c.json({ error: 'Federation is not enabled' }, 403); + } + return next(); +}); From 604921c243553bb056cc1f0efd5027acc8422534 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 23:31:18 -0300 Subject: [PATCH 53/99] refactor(reactions): callback signatures to include room parameter --- .../app/reactions/server/setReaction.ts | 16 ++++---------- .../ee/server/hooks/federation/index.ts | 21 ++++++++++++------- apps/meteor/lib/callbacks.ts | 6 +++--- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index cbad6863e2b9f..0559ccfefad74 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -33,13 +33,7 @@ export const removeUserReaction = (message: IMessage, reaction: string, username return message; }; -export async function setReaction( - room: Pick, - 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/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 72227f2153152..93af07fa31ef7 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -17,6 +17,7 @@ import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemo import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; import { beforeChangeRoomRole } from '../../../../lib/callbacks/beforeChangeRoomRole'; import { getFederationVersion } from '../../../../server/services/federation/utils'; +import { FederationActions } from '../../../../server/services/messages/hooks/BeforeFederationActions'; // callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); @@ -74,7 +75,7 @@ callbacks.add( ); callbacks.add( 'afterDeleteMessage', - async (message: IMessage) => { + async (message: IMessage, room) => { if (!message.federation?.eventId) { return; } @@ -122,12 +123,14 @@ beforeAddUserToRoom.add( callbacks.add( 'afterSetReaction', - async (message: IMessage, params: { user: IUser; reaction: string }): Promise => { + async (message: IMessage, params): Promise => { // Don't federate reactions that came from Matrix if (params.user.username?.includes(':')) { return; } - await FederationMatrix.sendReaction(message._id, params.reaction, params.user); + if (FederationActions.blockIfRoomFederatedButServiceNotReady(params.room)) { + await FederationMatrix.sendReaction(message._id, params.reaction, params.user); + } }, callbacks.priority.HIGH, 'federation-matrix-after-set-reaction', @@ -135,12 +138,14 @@ callbacks.add( callbacks.add( 'afterUnsetReaction', - async (_message: IMessage, params: { user: IUser; reaction: string; oldMessage: IMessage }): Promise => { + async (_message: IMessage, params): Promise => { // Don't federate reactions that came from Matrix if (params.user.username?.includes(':')) { return; } - await FederationMatrix.removeReaction(params.oldMessage._id, params.reaction, params.user, params.oldMessage); + if (FederationActions.blockIfRoomFederatedButServiceNotReady(params.room)) { + await FederationMatrix.removeReaction(params.oldMessage._id, params.reaction, params.user, params.oldMessage); + } }, callbacks.priority.HIGH, 'federation-matrix-after-unset-reaction', @@ -224,8 +229,10 @@ beforeChangeRoomRole.add( callbacks.add( 'beforeCreateDirectRoom', - async (members: IUser[] | string[]): Promise => { - await FederationMatrix.ensureFederatedUsersExistLocally(members); + async (members: IUser[] | string[], room): Promise => { + if (FederationActions.blockIfRoomFederatedButServiceNotReady(room)) { + await FederationMatrix.ensureFederatedUsersExistLocally(members); + } }, callbacks.priority.HIGH, 'federation-matrix-before-create-direct-room', diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index f1f8012293542..8aca3095551f0 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -78,12 +78,12 @@ interface EventLikeCallbackSignatures { options?: ICreateRoomOptions; }, ) => void; - 'beforeCreateDirectRoom': (members: IUser[]) => void; + 'beforeCreateDirectRoom': (members: IUser[], room: IRoom) => void; 'federation.beforeCreateDirectMessage': (members: IUser[]) => void; - 'afterSetReaction': (message: IMessage, { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean }) => void; + 'afterSetReaction': (message: IMessage, parems: { user: IUser; reaction: string; shouldReact: boolean; room: IRoom }) => void; 'afterUnsetReaction': ( message: IMessage, - { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean; oldMessage: IMessage }, + parems: { 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; From e6cc948269d397681c936eb67c67e483b27ef7dc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Sep 2025 23:21:28 -0300 Subject: [PATCH 54/99] refactor(federation): streamline room federation checks and enhance type safety --- .../lib/server/functions/createDirectRoom.ts | 4 +- .../ee/server/hooks/federation/index.ts | 126 ++++++++---------- .../room/hooks/BeforeFederationActions.ts | 23 +++- 3 files changed, 72 insertions(+), 81 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 5ebe3b0f639b4..b5207256c3a7a 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -59,8 +59,6 @@ export async function createDirectRoom( ); } - await callbacks.run('beforeCreateDirectRoom', members); - const membersUsernames: string[] = members .map((member) => { if (typeof member === 'string') { @@ -98,6 +96,8 @@ export async function createDirectRoom( ...roomExtraData, }; + await callbacks.run('beforeCreateDirectRoom', members, roomInfo); + if (isNewRoom) { const tmpRoom: { _USERNAMES?: (string | undefined)[] } & typeof roomInfo = { ...roomInfo, diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 93af07fa31ef7..b2c617c487113 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,13 +1,5 @@ import { api, FederationMatrix } from '@rocket.chat/core-services'; -import { - isEditedMessage, - isRoomNativeFederated, - isMessageFromMatrixFederation, - isRoomFederated, - type IMessage, - type IRoom, - type IUser, -} from '@rocket.chat/core-typings'; +import { isEditedMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { MatrixBridgedRoom, Rooms } from '@rocket.chat/models'; import notifications from '../../../../app/notifications/server/lib/Notifications'; @@ -17,17 +9,13 @@ import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemo import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; import { beforeChangeRoomRole } from '../../../../lib/callbacks/beforeChangeRoomRole'; import { getFederationVersion } from '../../../../server/services/federation/utils'; -import { FederationActions } from '../../../../server/services/messages/hooks/BeforeFederationActions'; +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 (!isRoomFederated(room)) { - return; - } - const federationVersion = getFederationVersion(); - if (federationVersion === 'native') { + if (FederationActions.shouldPerformFederationAction(room)) { const federatedRoomId = options?.federatedRoomId; // TODO: move this to the hooks folder setupTypingEventListenerForRoom(room._id); @@ -49,24 +37,22 @@ callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, origi callbacks.add( 'afterSaveMessage', async (message, { room, user }) => { - if (!isRoomFederated(room)) { - return; - } - - const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); - const federationVersion = getFederationVersion(); - - if (shouldBeHandledByFederation && federationVersion === 'native') { - 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); + if (FederationActions.shouldPerformFederationAction(room)) { + const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); + const federationVersion = getFederationVersion(); + + if (shouldBeHandledByFederation && federationVersion === 'native') { + 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); } - } 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); } } }, @@ -85,7 +71,9 @@ callbacks.add( return; } - await FederationMatrix.deleteMessage(message); + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.deleteMessage(message); + } }, callbacks.priority.MEDIUM, 'native-federation-after-delete-message', @@ -94,14 +82,13 @@ callbacks.add( callbacks.add( 'federation.onAddUsersToRoom', async ({ invitees, inviter }, room) => { - if (!isRoomFederated(room)) { - return; + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.inviteUsersToRoom( + room, + invitees.map((invitee) => (typeof invitee === 'string' ? invitee : (invitee.username as string))), + inviter, + ); } - await FederationMatrix.inviteUsersToRoom( - room, - invitees.map((invitee) => (typeof invitee === 'string' ? invitee : (invitee.username as string))), - inviter, - ); }, callbacks.priority.MEDIUM, 'native-federation-on-add-users-to-room ', @@ -112,10 +99,10 @@ beforeAddUserToRoom.add( if (!user.username || !inviter) { return; } - if (!isRoomNativeFederated(room)) { - return; + + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.inviteUsersToRoom(room, [user.username], inviter); } - await FederationMatrix.inviteUsersToRoom(room, [user.username], inviter); }, callbacks.priority.MEDIUM, 'native-federation-on-before-add-users-to-room', @@ -128,7 +115,7 @@ callbacks.add( if (params.user.username?.includes(':')) { return; } - if (FederationActions.blockIfRoomFederatedButServiceNotReady(params.room)) { + if (FederationActions.shouldPerformFederationAction(params.room)) { await FederationMatrix.sendReaction(message._id, params.reaction, params.user); } }, @@ -143,7 +130,7 @@ callbacks.add( if (params.user.username?.includes(':')) { return; } - if (FederationActions.blockIfRoomFederatedButServiceNotReady(params.room)) { + if (FederationActions.shouldPerformFederationAction(params.room)) { await FederationMatrix.removeReaction(params.oldMessage._id, params.reaction, params.user, params.oldMessage); } }, @@ -153,11 +140,9 @@ callbacks.add( afterLeaveRoomCallback.add( async (user: IUser, room: IRoom): Promise => { - if (!isRoomFederated(room)) { - return; + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.leaveRoom(room._id, user); } - - await FederationMatrix.leaveRoom(room._id, user); }, callbacks.priority.HIGH, 'federation-matrix-after-leave-room', @@ -165,11 +150,9 @@ afterLeaveRoomCallback.add( afterRemoveFromRoomCallback.add( async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise => { - if (!isRoomFederated(room)) { - return; + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved); } - - await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved); }, callbacks.priority.HIGH, 'federation-matrix-after-remove-from-room', @@ -178,7 +161,7 @@ afterRemoveFromRoomCallback.add( callbacks.add( 'afterRoomNameChange', async ({ room, name, userId }) => { - if (name && isRoomFederated(room)) { + if (FederationActions.shouldPerformFederationAction(room)) { await FederationMatrix.updateRoomName(room._id, name, userId); } }, @@ -189,10 +172,9 @@ callbacks.add( callbacks.add( 'afterRoomTopicChange', async (_, { room, topic, userId }) => { - if (!isRoomFederated(room)) { - return; + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.updateRoomTopic(room._id, topic, userId); } - await FederationMatrix.updateRoomTopic(room._id, topic, userId); }, callbacks.priority.HIGH, 'federation-matrix-after-room-topic-changed', @@ -200,17 +182,15 @@ callbacks.add( callbacks.add( 'afterSaveMessage', - async (message: IMessage, { room }): Promise => { - if (!room || !isRoomFederated(room) || !message || !isMessageFromMatrixFederation(message)) { - return message; - } + async (message: IMessage, { room }) => { + if (FederationActions.shouldPerformFederationAction(room)) { + if (!isEditedMessage(message)) { + return; + } + FederationActions.shouldPerformFederationAction(room); - if (!isEditedMessage(message)) { - return message; + await FederationMatrix.updateMessage(message._id, message.msg, message.u); } - - await FederationMatrix.updateMessage(message._id, message.msg, message.u); - return message; }, callbacks.priority.HIGH, 'federation-matrix-after-room-message-updated', @@ -218,10 +198,9 @@ callbacks.add( beforeChangeRoomRole.add( async (params: { fromUserId: string; userId: string; room: IRoom; role: 'moderator' | 'owner' | 'leader' | 'user' }) => { - if (!isRoomFederated(params.room)) { - return; + if (FederationActions.shouldPerformFederationAction(params.room)) { + await FederationMatrix.addUserRoleRoomScoped(params.room._id, params.fromUserId, params.userId, params.role); } - await FederationMatrix.addUserRoleRoomScoped(params.room._id, params.fromUserId, params.userId, params.role); }, callbacks.priority.HIGH, 'federation-matrix-before-change-room-role', @@ -229,8 +208,8 @@ beforeChangeRoomRole.add( callbacks.add( 'beforeCreateDirectRoom', - async (members: IUser[] | string[], room): Promise => { - if (FederationActions.blockIfRoomFederatedButServiceNotReady(room)) { + async (members, room): Promise => { + if (FederationActions.shouldPerformFederationAction(room)) { await FederationMatrix.ensureFederatedUsersExistLocally(members); } }, @@ -241,10 +220,9 @@ callbacks.add( callbacks.add( 'afterCreateDirectRoom', async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }): Promise => { - if (!isRoomFederated(room)) { - return; + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId); } - await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId); }, callbacks.priority.HIGH, 'federation-matrix-after-create-direct-room', diff --git a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts index ccad0af4c6c45..dbc0b8eb2b44e 100644 --- a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts @@ -1,15 +1,28 @@ -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 { throwIfFederationNotEnabledOrNotReady } 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(); From 8e192e65ec28d85f43581ff15e1325ae427e0e6c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Sep 2025 08:47:25 -0300 Subject: [PATCH 55/99] feat(federation): integrate middleware for federation and license checks in well-known routes --- ee/packages/federation-matrix/src/FederationMatrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index c2591bbab881c..05beb0cf9ab05 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -172,7 +172,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS .use(getKeyServerRoutes(this.homeserverServices)) .use(getFederationVersionsRoutes(this.homeserverServices)); - wellKnown.use(getWellKnownRoutes(this.homeserverServices)); + wellKnown.use(isFederationEnabledMiddleware).use(isLicenseEnabledMiddleware).use(getWellKnownRoutes(this.homeserverServices)); this.httpRoutes = { matrix, wellKnown }; } From eb51988c7b6574560c13936adf2c8f8e5ecfc788 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 19 Sep 2025 15:15:51 -0300 Subject: [PATCH 56/99] fix: add optional params to /query/profile (#36956) --- .../src/api/_matrix/profiles.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 60db4e87150e8..8cb242f444a21 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -30,6 +30,12 @@ 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, @@ -351,10 +357,19 @@ export const getMatrixProfilesRoutes = (services: HomeserverServices) => { license: ['federation'], }, async (c) => { - const { user_id: userId } = c.req.query(); + 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, From 65717c64f242a7669a6dc576b3992f7ae43a2709 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 19 Sep 2025 15:25:37 -0300 Subject: [PATCH 57/99] feat: adds federation event ACL (#36913) --- .../src/api/_matrix/transactions.ts | 9 +++-- .../federation-matrix/src/api/middlewares.ts | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 ee/packages/federation-matrix/src/api/middlewares.ts diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 27a58a054b8dc..31f94e0bd454f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -3,6 +3,8 @@ import type { EventID } from '@hs/room'; 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: { @@ -252,7 +254,7 @@ const GetStateResponseSchema = { const isGetStateResponseProps = ajv.compile(GetStateResponseSchema); export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { - const { event, config } = services; + const { event, federationAuth } = services; // PUT /_matrix/federation/v1/send/{txnId} return ( @@ -373,6 +375,7 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { tags: ['Federation'], license: ['federation'], }, + canAccessEvent(federationAuth), async (c) => { const eventData = await event.getEventById(c.req.param('eventId') as EventID); if (!eventData) { @@ -387,8 +390,8 @@ export const getMatrixTransactionsRoutes = (services: HomeserverServices) => { return { body: { - origin_server_ts: new Date().getTime(), - origin: config.serverName, + 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/middlewares.ts b/ee/packages/federation-matrix/src/api/middlewares.ts new file mode 100644 index 0000000000000..bcfff8dada781 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares.ts @@ -0,0 +1,33 @@ +import type { EventAuthorizationService } from '@hs/federation-sdk'; +import { errCodes } from '@hs/federation-sdk'; +import type { EventID } from '@hs/room'; +import type { Context, Next } from 'hono'; + +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); + } +}; From e5d5cb55bb91cfaccfd53dccb5fd63650cd02112 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Sep 2025 17:51:35 -0300 Subject: [PATCH 58/99] feat(federation): add federation versioning to user and room models --- apps/meteor/server/lib/ldap/Manager.ts | 3 +++ ee/packages/federation-matrix/src/FederationMatrix.ts | 3 +++ ee/packages/federation-matrix/src/events/message.ts | 3 +++ ee/packages/federation-matrix/src/helpers/identifiers.ts | 3 +++ packages/core-typings/src/IRoom.ts | 2 +- packages/core-typings/src/IUser.ts | 2 +- packages/models/src/models/Rooms.ts | 6 ++++++ packages/models/src/models/Users.ts | 3 +++ 8 files changed, 23 insertions(+), 2 deletions(-) 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/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 05beb0cf9ab05..546182a666b36 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -284,6 +284,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS roles: ['user'], requirePasswordChange: false, federated: true, + federation: { + version: 1, + }, createdAt: new Date(), _updatedAt: new Date(), }; diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 758b727226059..42c4f001d97dd 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -47,6 +47,9 @@ export function message(emitter: Emitter, serverName: roles: ['user'], requirePasswordChange: false, federated: true, // Mark as federated user + federation: { + version: 1, + }, createdAt: new Date(), _updatedAt: new Date(), }; diff --git a/ee/packages/federation-matrix/src/helpers/identifiers.ts b/ee/packages/federation-matrix/src/helpers/identifiers.ts index 99957f159348f..781d3831194cc 100644 --- a/ee/packages/federation-matrix/src/helpers/identifiers.ts +++ b/ee/packages/federation-matrix/src/helpers/identifiers.ts @@ -66,6 +66,9 @@ export async function saveLocalUserForExternalUserId(externalUserId: string, ori name: getLocalNameForMatrixUserIdToSave(externalUserId), requirePasswordChange: false, federated: true, + federation: { + version: 1, + }, createdAt: new Date(), _updatedAt: new Date(), }; diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 6159362ad9cca..7f3ca6348c233 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -113,7 +113,7 @@ export interface IRoomFederated extends IRoom { export interface IRoomNativeFederated extends IRoomFederated { federation: { - version: `${number}`; + version: number; }; } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 5f961011f0af6..546bc183d97a6 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -227,7 +227,7 @@ export interface IUser extends IRocketChatRecord { federated?: boolean; // @deprecated federation?: { - version?: `${number}`; + version?: number; avatarUrl?: string; searchedServerNames?: string[]; }; diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index ef0d3e1f9feab..8529cf674d8c1 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -1969,6 +1969,12 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { _id: (await this.insertOne(room)).insertedId, _updatedAt: new Date(), ...room, + ...(room.federated && { + federated: true, + federation: { + version: 1, + }, + }), }; return newRoom; 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); From c30521df1a731b8570ed09646f2efba49e7471a6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Sep 2025 18:17:00 -0300 Subject: [PATCH 59/99] fix: user federation version field --- packages/core-typings/src/IUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 546bc183d97a6..bab6a3e40b461 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -263,7 +263,7 @@ export const isUserFederated = (user: Partial | Partial export interface IUserNativeFederated extends IUser { federated: true; federation: { - version: `${number}`; + version: number; }; } From ad201fa94ede6297542504c39a1c22529f4b11ff Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 19 Sep 2025 18:20:26 -0300 Subject: [PATCH 60/99] refactor: set tshow to true for the first thread message in federated rooms (#37008) Co-authored-by: Guilherme Gazzo --- apps/meteor/server/services/messages/service.ts | 7 +++---- .../federation-matrix/src/events/message.ts | 15 ++++++++------- .../core-services/src/types/IMessageService.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 29767fe0f32a5..94c543da63593 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -90,19 +90,18 @@ export class MessageService extends ServiceClassInternal implements IMessageServ rid, msg, federation_event_id, - tmid, + thread, }: { fromId: string; rid: string; msg: string; federation_event_id: string; - tmid?: string; + thread?: { tmid: string; tshow: boolean }; }): Promise { - const threadParams = tmid ? { tmid, tshow: true } : {}; return executeSendMessage(fromId, { rid, msg, - ...threadParams, + ...thread, federation: { eventId: federation_event_id }, }); } diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 42c4f001d97dd..b1eeb4c305f46 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -114,15 +114,16 @@ export function message(emitter: Emitter, serverName: } } - let tmid: string | undefined; + let thread: { tmid: string; tshow: boolean } | undefined; if (isThreadMessage && threadRootEventId) { const threadRootMessage = await Messages.findOneByFederationId(threadRootEventId); - if (threadRootMessage) { - tmid = threadRootMessage._id; - logger.debug('Found thread root message:', { tmid, threadRootEventId }); - } else { + if (!threadRootMessage) { logger.warn('Thread root message not found for event:', threadRootEventId); + return; } + + const shouldSetTshow = !threadRootMessage?.tcount; + thread = { tmid: threadRootMessage._id, tshow: shouldSetTshow }; } const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; @@ -201,7 +202,7 @@ export function message(emitter: Emitter, serverName: rid: internalRoomId, msg: formatted, federation_event_id: data.event_id, - tmid, + thread, }); return; } @@ -217,7 +218,7 @@ export function message(emitter: Emitter, serverName: rid: internalRoomId, msg: formatted, federation_event_id: data.event_id, - tmid, + thread, }); } catch (error) { logger.error('Error processing Matrix message:', error); diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 0b2e5a743b11f..223dbc92dcfbd 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -14,13 +14,13 @@ export interface IMessageService { rid, msg, federation_event_id, - tmid, + thread, }: { fromId: string; rid: string; msg: string; federation_event_id: string; - tmid?: string; + thread?: { tmid: string; tshow: boolean }; }): Promise; saveSystemMessageAndNotifyUser( type: MessageTypesValues, From caf9a2476209ca61332fa8879a50ca865bbda585 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 19 Sep 2025 18:37:51 -0300 Subject: [PATCH 61/99] feat: add Federation allow list (#37010) --- .../server/settings/federation-service.ts | 7 +++ .../federation-matrix/src/FederationMatrix.ts | 2 + .../middlewares/isFederationDomainAllowed.ts | 61 +++++++++++++++++++ packages/i18n/src/locales/en.i18n.json | 2 + 4 files changed, 72 insertions(+) create mode 100644 ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 65174cf6f008d..3a47d15751d95 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -24,5 +24,12 @@ export const createFederationServiceSettings = async (): Promise => { i18nDescription: 'Federation_Service_Matrix_Signing_Key_Description', public: false, }); + + await this.add('Federation_Service_Allow_List', '', { + type: 'string', + i18nLabel: 'Federation_Service_Allow_List', + i18nDescription: 'Federation_Service_Allow_List_Description', + public: false, + }); }); }; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 546182a666b36..0faf1f69a0027 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -21,6 +21,7 @@ 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'; @@ -164,6 +165,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS matrix .use(isFederationEnabledMiddleware) .use(isLicenseEnabledMiddleware) + .use(isFederationDomainAllowedMiddleware) .use(getMatrixInviteRoutes(this.homeserverServices)) .use(getMatrixProfilesRoutes(this.homeserverServices)) .use(getMatrixRoomsRoutes(this.homeserverServices)) 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/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 91bc949433346..1693ec0f8108f 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2163,6 +2163,8 @@ "Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.", "Federation_Service_Matrix_Signing_Key": "Matrix server signing key", "Federation_Service_Matrix_Signing_Key_Description": "The private signing key used by your Matrix server to authenticate federation requests. Format should be: algorithm version base64. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated Matrix servers and should be kept confidential.", + "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", From 29af9868f2bfafb46a7590c54dd02d1275a53f66 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Sat, 20 Sep 2025 04:18:53 +0530 Subject: [PATCH 62/99] fix: dm check on invite (#37012) Co-authored-by: Guilherme Gazzo --- .../src/api/_matrix/invite.ts | 47 ++----------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 7175e9936ad27..115caa1f564d6 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -1,5 +1,5 @@ import type { HomeserverServices, RoomService, StateService } from '@hs/federation-sdk'; -import type { PersistentEventBase } from '@hs/room'; +import type { PduMembershipEventContent, PersistentEventBase, RoomVersion } from '@hs/room'; import { Room } from '@rocket.chat/core-services'; import type { IUser, UserStatus } from '@rocket.chat/core-typings'; import { Router } from '@rocket.chat/http-router'; @@ -143,52 +143,13 @@ async function runWithBackoff(fn: () => Promise, delaySec = 5) { } } -async function isDirectMessage(matrixRoom: unknown, inviteEvent: PersistentEventBase): Promise { - try { - const room = matrixRoom as { getEvent?: (type: string) => { content?: { is_direct?: boolean } } }; - const creationEvent = room.getEvent?.('m.room.create'); - if (creationEvent?.content?.is_direct) { - return true; - } - - const roomWithEvents = matrixRoom as { getEvents?: (type: string) => Array<{ content?: { membership?: string }; stateKey?: string }> }; - const memberEvents = roomWithEvents.getEvents?.('m.room.member'); - const joinedMembers = - memberEvents?.filter((event) => event.content?.membership === 'join' || event.content?.membership === 'invite') || []; - - if (joinedMembers.length === 2) { - return true; - } - - const uniqueUsers = new Set(); - if (memberEvents) { - for (const event of memberEvents) { - if (event.content?.membership === 'join' || event.content?.membership === 'invite') { - if (event.stateKey) { - uniqueUsers.add(event.stateKey); - } - } - } - } - uniqueUsers.add(inviteEvent.sender); - if (inviteEvent.stateKey) { - uniqueUsers.add(inviteEvent.stateKey); - } - - return uniqueUsers.size <= 2; - } catch (error) { - console.log('Could not determine if room is DM:', error); - return false; - } -} - async function joinRoom({ inviteEvent, user, // ours trying to join the room room, state, }: { - inviteEvent: PersistentEventBase; + inviteEvent: PersistentEventBase; user: IUser; room: RoomService; state: StateService; @@ -208,7 +169,7 @@ async function joinRoom({ } // we only understand these two types of rooms, plus direct messages - const isDM = await isDirectMessage(matrixRoom, inviteEvent); + 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'); @@ -392,7 +353,7 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); void startJoiningRoom({ - inviteEvent, + inviteEvent: inviteEvent as PersistentEventBase, // TODO: change the processInvite return type user: ourUser, room, state, From f3730c3b2d4b21af55615bff5087fca4ea270746 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Sep 2025 22:14:37 -0300 Subject: [PATCH 63/99] chore: publish federation fields for room --- apps/meteor/lib/publishFields.ts | 1 + 1 file changed, 1 insertion(+) 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, From 23898d196664efa456021c88b73f5fc0e7df0068 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 19 Sep 2025 22:17:31 -0300 Subject: [PATCH 64/99] feat: adds federation files support (#36842) Co-authored-by: Diego Sampaio Co-authored-by: Guilherme Gazzo --- .../client/lib/e2ee/rocketchat.e2e.room.ts | 13 +- apps/meteor/client/lib/e2ee/rocketchat.e2e.ts | 4 +- .../server/services/messages/service.ts | 9 + .../federation-matrix/src/FederationMatrix.ts | 232 ++++++++---- .../src/api/_matrix/media.ts | 153 ++++++++ .../federation-matrix/src/api/middlewares.ts | 29 ++ .../federation-matrix/src/events/message.ts | 355 ++++++++++++------ .../src/services/MatrixMediaService.ts | 148 ++++++++ .../src/types/IMessageService.ts | 6 + .../core-typings/src/IMessage/IMessage.ts | 14 + packages/core-typings/src/IUpload.ts | 5 + .../src/models/IMessagesModel.ts | 2 + .../model-typings/src/models/IUploadsModel.ts | 6 +- packages/models/src/models/Messages.ts | 13 + packages/models/src/models/Uploads.ts | 17 +- 15 files changed, 802 insertions(+), 204 deletions(-) create mode 100644 ee/packages/federation-matrix/src/api/_matrix/media.ts create mode 100644 ee/packages/federation-matrix/src/services/MatrixMediaService.ts 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/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 94c543da63593..85938ba38fb0f 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -90,12 +90,18 @@ export class MessageService extends ServiceClassInternal implements IMessageServ 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, { @@ -103,6 +109,9 @@ export class MessageService extends ServiceClassInternal implements IMessageServ msg, ...thread, federation: { eventId: federation_event_id }, + ...(file && { file }), + ...(files && { files }), + ...(attachments && { attachments }), }); } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 0faf1f69a0027..a2cf3f92b4f1c 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -16,6 +16,7 @@ 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'; @@ -27,6 +28,16 @@ import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; import { registerEvents } from './events'; import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; +import { MatrixMediaService } from './services/MatrixMediaService'; + +type MatrixFileTypes = 'm.image' | 'm.video' | 'm.audio' | 'm.file'; + +export const fileTypes: Record = { + image: 'm.image', + video: 'm.video', + audio: 'm.audio', + file: 'm.file', +}; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; @@ -101,6 +112,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS await createFederationContainer(containerOptions, config); instance.homeserverServices = getAllServices(); + MatrixMediaService.setHomeserverServices(instance.homeserverServices); instance.buildMatrixHTTPRoutes(); instance.onEvent('user.typing', async ({ isTyping, roomId, user: { username } }): Promise => { if (!roomId || !username) { @@ -172,7 +184,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS .use(getMatrixSendJoinRoutes(this.homeserverServices)) .use(getMatrixTransactionsRoutes(this.homeserverServices)) .use(getKeyServerRoutes(this.homeserverServices)) - .use(getFederationVersionsRoutes(this.homeserverServices)); + .use(getFederationVersionsRoutes(this.homeserverServices)) + .use(getMatrixMediaRoutes(this.homeserverServices)); wellKnown.use(isFederationEnabledMiddleware).use(isLicenseEnabledMiddleware).use(getWellKnownRoutes(this.homeserverServices)); @@ -398,6 +411,143 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } + private getMatrixMessageType(mimeType?: string): MatrixFileTypes { + 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 { + // TODO: Handle multiple files + const file = message.files[0]; + const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain); + + const msgtype = this.getMatrixMessageType(file.type); + const fileContent = { + body: file.name, + msgtype, + url: mxcUri, + info: { + mimetype: file.type, + size: file.size, + }, + }; + + return this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId); + } 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: IRoom, user: IUser): Promise { try { const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); @@ -419,84 +569,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const actualMatrixUserId = existingMatrixUserId || matrixUserId; let result; - - const parsedMessage = await toExternalMessageFormat({ - message: message.msg, - externalRoomId: matrixRoomId, - homeServerDomain: this.serverName, - }); - if (!message.tmid) { - if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); - if (!quoteMessage) { - throw new Error('Failed to retrieve quote message'); - } - result = await this.homeserverServices.message.sendReplyToMessage( - matrixRoomId, - quoteMessage.rawMessage, - quoteMessage.formattedMessage, - quoteMessage.eventToReplyTo, - actualMatrixUserId, - ); - } else { - result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId); - } + if (message.files && message.files.length > 0) { + result = await this.handleFileMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); } else { - const threadRootMessage = await Messages.findOneById(message.tmid); - const threadRootEventId = threadRootMessage?.federation?.eventId; - - if (threadRootEventId) { - const latestThreadMessage = await Messages.findOne( - { - 'tmid': message.tmid, - 'federation.eventId': { $exists: true }, - '_id': { $ne: message._id }, // Exclude the current message - }, - { sort: { ts: -1 } }, - ); - const latestThreadEventId = latestThreadMessage?.federation?.eventId; - - if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); - if (!quoteMessage) { - throw new Error('Failed to retrieve quote message'); - } - result = await this.homeserverServices.message.sendReplyToInsideThreadMessage( - matrixRoomId, - quoteMessage.rawMessage, - quoteMessage.formattedMessage, - actualMatrixUserId, - threadRootEventId, - quoteMessage.eventToReplyTo, - ); - } else { - result = await this.homeserverServices.message.sendThreadMessage( - matrixRoomId, - message.msg, - parsedMessage, - actualMatrixUserId, - threadRootEventId, - latestThreadEventId, - ); - } - } else { - this.logger.warn('Thread root event ID not found, sending as regular message'); - if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) { - const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); - if (!quoteMessage) { - throw new Error('Failed to retrieve quote message'); - } - result = await this.homeserverServices.message.sendReplyToMessage( - matrixRoomId, - quoteMessage.rawMessage, - quoteMessage.formattedMessage, - quoteMessage.eventToReplyTo, - actualMatrixUserId, - ); - } else { - result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId); - } - } + result = await this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); } if (!result) { 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..650452ca09844 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/_matrix/media.ts @@ -0,0 +1,153 @@ +import crypto from 'crypto'; + +import type { HomeserverServices } from '@hs/federation-sdk'; +import type { IUpload } from '@rocket.chat/core-typings'; +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/middlewares.ts b/ee/packages/federation-matrix/src/api/middlewares.ts index bcfff8dada781..a919cc0758103 100644 --- a/ee/packages/federation-matrix/src/api/middlewares.ts +++ b/ee/packages/federation-matrix/src/api/middlewares.ts @@ -3,6 +3,35 @@ import { errCodes } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; 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); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index b1eeb4c305f46..aa99a2b79c81d 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,131 +1,252 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, IRoom } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, 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'); -export function message(emitter: Emitter, serverName: string) { - emitter.on('homeserver.matrix.message', async (data) => { - try { - const message = data.content?.body?.toString(); - if (!message) { - logger.debug('No message found in event content'); - return; - } +async function getOrCreateFederatedUser(matrixUserId: string): Promise { + const [userPart, domain] = matrixUserId.split(':'); + if (!userPart || !domain) { + logger.error('Invalid Matrix sender ID format:', matrixUserId); + return null; + } + const username = userPart.substring(1); - const content = data.content as any; - const replyToRelation = content?.['m.relates_to']; - const isThreadMessage = replyToRelation?.rel_type === 'm.thread'; - const isQuoteMessage = replyToRelation?.['m.in_reply_to']?.event_id && !replyToRelation?.is_falling_back; - const threadRootEventId = isThreadMessage ? replyToRelation.event_id : undefined; + const user = await Users.findOneByUsername(matrixUserId); + if (user) { + await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, false, domain); + return user; + } - const [userPart, domain] = data.sender.split(':'); - if (!userPart || !domain) { - logger.error('Invalid Matrix sender ID format:', data.sender); - return; - } - const username = userPart.substring(1); + logger.info('Creating new federated user:', { username: matrixUserId, externalId: matrixUserId }); - const internalUsername = data.sender; - let user = await Users.findOneByUsername(internalUsername); + const userData = { + username: matrixUserId, + name: username, // TODO: Fetch display name from Matrix profile + type: 'user', + status: UserStatus.ONLINE, + active: true, + roles: ['user'], + requirePasswordChange: false, + federated: true, + federation: { + version: 1, + }, + createdAt: new Date(), + _updatedAt: new Date(), + }; - if (!user) { - logger.info('Creating new federated user:', { username: internalUsername, externalId: data.sender }); - - const userData: Partial = { - username: internalUsername, - name: username, // TODO: Fetch display name from Matrix profile - type: 'user', - status: UserStatus.ONLINE, - active: true, - roles: ['user'], - requirePasswordChange: false, - federated: true, // Mark as federated user - federation: { - version: 1, - }, - createdAt: new Date(), - _updatedAt: new Date(), - }; + const { insertedId } = await Users.insertOne(userData); - const { insertedId } = await Users.insertOne(userData as IUser); + await MatrixBridgedUser.createOrUpdateByLocalId( + insertedId, + matrixUserId, + true, // isRemote = true for external Matrix users + domain, + ); - await MatrixBridgedUser.createOrUpdateByLocalId( - insertedId, - data.sender, - true, // isRemote = true for external Matrix users - domain, - ); + const newUser = await Users.findOneById(insertedId); + if (!newUser) { + logger.error('Failed to create user:', matrixUserId); + return null; + } - user = await Users.findOneById(insertedId); - if (!user) { - logger.error('Failed to create user:', internalUsername); - return; - } + logger.info('Successfully created federated user:', { userId: newUser._id, username }); - logger.info('Successfully created federated user:', { userId: user._id, username }); - } else { - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, data.sender, false, domain); - } + return newUser; +} - const internalRoomId = await MatrixBridgedRoom.getLocalRoomId(data.room_id); - if (!internalRoomId) { - logger.error('Room not found in bridge mapping:', data.room_id); - // TODO: Handle room creation for unknown federated rooms - return; - } +async function getRoomAndEnsureSubscription(matrixRoomId: string, user: IUser): Promise { + const internalRoomId = await MatrixBridgedRoom.getLocalRoomId(matrixRoomId); + if (!internalRoomId) { + logger.error('Room not found in bridge mapping:', matrixRoomId); + // TODO: Handle room creation for unknown federated rooms + return null; + } - const room = await Rooms.findOneById(internalRoomId); - if (!room) { - logger.error('Room not found:', internalRoomId); - return; - } + const room = await Rooms.findOneById(internalRoomId); + if (!room) { + logger.error('Room not found:', internalRoomId); + return null; + } - if (!room.federated) { - logger.error('Room is not marked as federated:', { roomId: room._id, matrixRoomId: data.room_id }); - // TODO: Should we update the room to be federated? - } + if (!room.federated) { + logger.error('Room is not marked as federated:', { roomId: room._id, matrixRoomId }); + // TODO: Should we update the room to be federated? + } - const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); + const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); - if (!existingSubscription) { - logger.info('Creating subscription for federated user in room:', { userId: user._id, roomId: room._id }); + if (existingSubscription) { + return room; + } - const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { - ts: new Date(), - open: false, - alert: false, - unread: 0, - userMentions: 0, - groupMentions: 0, - // Federation status is inherited from room.federated and user.federated - }); + logger.info('Creating subscription for federated user in room:', { userId: user._id, roomId: room._id }); - if (insertedId) { - logger.debug('Successfully created subscription:', insertedId); - // TODO: Import and use notifyOnSubscriptionChangedById if needed - // void notifyOnSubscriptionChangedById(insertedId, 'inserted'); - } + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { + ts: new Date(), + open: false, + alert: false, + unread: 0, + userMentions: 0, + groupMentions: 0, + }); + + if (insertedId) { + logger.debug('Successfully created subscription:', insertedId); + // TODO: Import and use notifyOnSubscriptionChangedById if needed + } + + return room; +} + +async function getThreadMessageId(threadRootEventId: string): 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( + // TODO improve typing + content: any, + msgtype: string, + messageBody: string, + user: IUser, + room: IRoom, + eventId: string, + tmid?: string, +): Promise<{ + fromId: string; + rid: string; + msg: string; + federation_event_id: string; + tmid?: string; + file: any; + files: any[]; + attachments: any[]; +}> { + const fileInfo = content.info; + const mimeType = fileInfo.mimetype; + const fileName = messageBody; + + const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, { + name: messageBody, + size: fileInfo.size, + type: mimeType, + roomId: room._id, + userId: user._id, + }); + + let fileExtension = ''; + if (fileName && fileName.includes('.')) { + fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; + } else if (mimeType && mimeType.includes('/')) { + fileExtension = mimeType.split('/')[1] || ''; + if (fileExtension === 'jpeg') { + fileExtension = 'jpg'; + } + } + + const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`; + + // TODO improve typing + const attachment: any = { + title: fileName, + type: 'file', + title_link: fileUrl, + title_link_download: true, + }; + + if (msgtype === 'm.image') { + attachment.image_url = fileUrl; + attachment.image_type = mimeType; + attachment.image_size = fileInfo.size || 0; + attachment.description = ''; + if (fileInfo.w && fileInfo.h) { + attachment.image_dimensions = { + width: fileInfo.w, + height: fileInfo.h, + }; + } + } else if (msgtype === 'm.video') { + attachment.video_url = fileUrl; + attachment.video_type = mimeType; + attachment.video_size = fileInfo.size || 0; + attachment.description = ''; + } else if (msgtype === 'm.audio') { + attachment.audio_url = fileUrl; + attachment.audio_type = mimeType; + attachment.audio_size = fileInfo.size || 0; + attachment.description = ''; + } else { + attachment.description = ''; + } + + const fileData = { + _id: fileRefId, + name: fileName, + type: mimeType, + size: fileInfo.size || 0, + format: fileExtension, + }; + + return { + fromId: user._id, + rid: room._id, + msg: '', + federation_event_id: eventId, + tmid, + file: fileData, + files: [fileData], + attachments: [attachment], + }; +} + +export function message(emitter: Emitter, serverName: string) { + emitter.on('homeserver.matrix.message', async (data) => { + try { + // TODO remove type casting + const content = data.content as any; + const msgtype = content?.msgtype; + const messageBody = content?.body?.toString(); + + if (!messageBody && !msgtype) { + logger.debug('No message content found in event'); + return; } - let thread: { tmid: string; tshow: boolean } | undefined; - if (isThreadMessage && threadRootEventId) { - const threadRootMessage = await Messages.findOneByFederationId(threadRootEventId); - if (!threadRootMessage) { - logger.warn('Thread root message not found for event:', threadRootEventId); - return; - } + const user = await getOrCreateFederatedUser(data.sender); + if (!user) { + return; + } - const shouldSetTshow = !threadRootMessage?.tcount; - thread = { tmid: threadRootMessage._id, tshow: shouldSetTshow }; + const room = await getRoomAndEnsureSubscription(data.room_id, user); + if (!room) { + return; } + const replyToRelation = content?.['m.relates_to']; + const threadRelation = content?.['m.relates_to']; + const isThreadMessage = threadRelation?.rel_type === 'm.thread'; + const isQuoteMessage = replyToRelation?.['m.in_reply_to']?.event_id && !replyToRelation?.is_falling_back; + const threadRootEventId = isThreadMessage ? threadRelation.event_id : undefined; + const thread = await getThreadMessageId(threadRootEventId); + + const isMediaMessage = Object.values(fileTypes).includes(msgtype); + const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { logger.debug('Received edited message from Matrix, updating existing message'); @@ -152,7 +273,7 @@ export function message(emitter: Emitter, serverName: const formatted = await toInternalQuoteMessageFormat({ messageToReplyToUrl, formattedMessage: data.content.formatted_body || '', - rawMessage: message, + rawMessage: messageBody, homeServerDomain: serverName, senderExternalId: data.sender, }); @@ -183,23 +304,24 @@ export function message(emitter: Emitter, serverName: ); return; } + if (isQuoteMessage && room.name) { const originalMessage = await Messages.findOneByFederationId(replyToRelation?.['m.in_reply_to']?.event_id); if (!originalMessage) { - logger.error('Original message not found for edit:', replyToRelation?.['m.in_reply_to']?.event_id); + logger.error('Original message not found for quote:', replyToRelation?.['m.in_reply_to']?.event_id); 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: message, + rawMessage: messageBody, homeServerDomain: serverName, senderExternalId: data.sender, }); await Message.saveMessageFromFederation({ fromId: user._id, - rid: internalRoomId, + rid: room._id, msg: formatted, federation_event_id: data.event_id, thread, @@ -207,19 +329,24 @@ export function message(emitter: Emitter, serverName: return; } - const formatted = await toInternalMessageFormat({ - rawMessage: message, - formattedMessage: data.content.formatted_body || '', - homeServerDomain: serverName, - senderExternalId: data.sender, - }); - await Message.saveMessageFromFederation({ - fromId: user._id, - rid: internalRoomId, - msg: formatted, - federation_event_id: data.event_id, - thread, - }); + if (isMediaMessage && content?.url) { + const result = await handleMediaMessage(content, msgtype, messageBody, user, room, 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); } 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..6c7aed293e421 --- /dev/null +++ b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts @@ -0,0 +1,148 @@ +import type { HomeserverServices } from '@hs/federation-sdk'; +import { Upload } from '@rocket.chat/core-services'; +import type { IUpload } from '@rocket.chat/core-typings'; +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): 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, { + 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, + 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, + 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/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 223dbc92dcfbd..793781bcfe070 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -14,12 +14,18 @@ export interface IMessageService { 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( diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index d1dc67b00230c..ae265f26d77ab 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -237,6 +237,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; } diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 8bcabee2fa8de..2eb5cf0741a73 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -59,6 +59,11 @@ export interface IUpload { hashes?: { sha256: string; }; + federation?: { + mxcUri: string; + serverName: string; + mediaId: string; + }; } export type IUploadWithUser = IUpload & { user?: Pick }; 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/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/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/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( { From 075ef6148287d06f0946d48ecbdc44f826b995ab Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Sep 2025 00:35:38 -0300 Subject: [PATCH 65/99] fix: loop caused by service federation adding users to room --- .../federation-matrix/src/FederationMatrix.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index a2cf3f92b4f1c..8ad7ffd9c94cf 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -4,7 +4,7 @@ import type { PresenceState } from '@hs/core'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; -import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; +import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; import { isDeletedMessage, isMessageFromMatrixFederation, isQuoteAttachment, UserStatus } from '@rocket.chat/core-typings'; import type { MessageQuoteAttachment, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; @@ -162,7 +162,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS presence: statusMap[user.status] || 'offline', }, ], - roomsUserIsMemberOf.map(({ externalRoomId }) => externalRoomId), + roomsUserIsMemberOf.map(({ externalRoomId }) => externalRoomId).filter(Boolean), ); }, ); @@ -670,29 +670,19 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const inviterUserId = `@${inviter.username}:${this.serverName}`; await Promise.all( - usersUserName.map(async (username) => { - const alreadyMember = await Subscriptions.findOneByRoomIdAndUsername(room._id, username, { projection: { _id: 1 } }); - if (alreadyMember) { - return; - } + 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; + } - const isExternalUser = username.includes(':'); - if (isExternalUser) { await this.homeserverServices.invite.inviteUserToRoom(username, matrixRoomId, inviterUserId); - return; - } - - const localUser = await Users.findOneByUsername(username, { projection: { _id: 1 } }); - if (localUser) { - await Room.addUserToRoom(room._id, localUser, { _id: inviter._id, username: inviter.username }); - let externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); - if (!externalUserId) { - externalUserId = `@${username}:${this.serverName}`; - await MatrixBridgedUser.createOrUpdateByLocalId(localUser._id, externalUserId, false, this.serverName); - } - await this.homeserverServices.invite.inviteUserToRoom(externalUserId, matrixRoomId, inviterUserId); - } - }), + }), ); } catch (error) { this.logger.error('Failed to invite an user to Matrix:', error); From 9ad3ba942eac9a091f5b556877fd48cfead6f14b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Sep 2025 11:05:35 -0300 Subject: [PATCH 66/99] fix(federation): add missing federation fields (#37015) --- .../app/lib/server/functions/createRoom.ts | 6 ++++++ .../meteor/server/services/federation/utils.ts | 18 ------------------ .../messages/hooks/BeforeFederationActions.ts | 6 +++--- .../src/api/_matrix/invite.ts | 3 +++ .../federation-matrix/src/events/invite.ts | 3 +++ 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 7eca89ae33761..d7da31646d451 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -191,6 +191,12 @@ export const createRoom = async ( fname: name, _updatedAt: now, ...extraData, + ...(extraData.federated && { + federated: true, + federation: { + version: 1, + }, + }), name: isDiscussion ? name : await getValidRoomName(name.trim(), undefined), t: type, msgs: 0, diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts index b997cbac84a8e..d36f98961b8d4 100644 --- a/apps/meteor/server/services/federation/utils.ts +++ b/apps/meteor/server/services/federation/utils.ts @@ -34,24 +34,6 @@ export function throwIfFederationNotEnabledOrNotReady(): 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/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 115caa1f564d6..e44d548eaf196 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -211,6 +211,9 @@ async function joinRoom({ name: inviteEvent.sender, requirePasswordChange: false, federated: true, + federation: { + version: 1, + }, createdAt: new Date(), _updatedAt: new Date(), }; diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index 12a40d10e03a3..225b475f9bbf6 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -30,6 +30,9 @@ export function invite(emitter: Emitter) { createdAt: new Date(), _updatedAt: new Date(), federated: true, + federation: { + version: 1, + }, }); const serverName = data.sender.split(':')[1] || 'unknown'; const bridgedUser = await MatrixBridgedUser.findOne({ mui: data.sender }); From ef76e88c99ff80afea89b50471d6cdb9b99c8133 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 22 Sep 2025 11:33:33 -0300 Subject: [PATCH 67/99] chore: organize federation settings (#36991) Co-authored-by: Guilherme Gazzo --- .gitignore | 3 +- .../server/services/federation/Settings.ts | 158 ++++++++++++++++++ .../server/settings/federation-service.ts | 46 +++-- apps/meteor/server/settings/federation.ts | 10 +- apps/meteor/server/settings/index.ts | 10 +- .../federation-matrix/src/FederationMatrix.ts | 4 +- packages/i18n/src/locales/ar.i18n.json | 3 - packages/i18n/src/locales/ca.i18n.json | 2 - packages/i18n/src/locales/cs.i18n.json | 2 - packages/i18n/src/locales/da.i18n.json | 2 - packages/i18n/src/locales/de-IN.i18n.json | 2 - packages/i18n/src/locales/de.i18n.json | 4 - packages/i18n/src/locales/en.i18n.json | 24 +-- packages/i18n/src/locales/es.i18n.json | 2 - packages/i18n/src/locales/fi.i18n.json | 4 - packages/i18n/src/locales/fr.i18n.json | 2 - packages/i18n/src/locales/hi-IN.i18n.json | 4 - packages/i18n/src/locales/hu.i18n.json | 4 - packages/i18n/src/locales/ja.i18n.json | 2 - packages/i18n/src/locales/ka-GE.i18n.json | 2 - packages/i18n/src/locales/ko.i18n.json | 2 - packages/i18n/src/locales/nb.i18n.json | 4 - packages/i18n/src/locales/nl.i18n.json | 2 - packages/i18n/src/locales/nn.i18n.json | 4 - packages/i18n/src/locales/pl.i18n.json | 4 - packages/i18n/src/locales/pt-BR.i18n.json | 4 - packages/i18n/src/locales/pt.i18n.json | 2 - packages/i18n/src/locales/ru.i18n.json | 3 - packages/i18n/src/locales/sv.i18n.json | 4 - packages/i18n/src/locales/uk.i18n.json | 2 - packages/i18n/src/locales/zh-TW.i18n.json | 2 - packages/i18n/src/locales/zh.i18n.json | 2 - 32 files changed, 226 insertions(+), 99 deletions(-) create mode 100644 apps/meteor/server/services/federation/Settings.ts diff --git a/.gitignore b/.gitignore index 819869a58a86f..300ae136bbab6 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ registration.yaml storybook-static development/tempo-data/ -homeserver \ No newline at end of file +homeserver +.env 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/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 3a47d15751d95..a117c9d96a61d 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -1,28 +1,50 @@ import { settingsRegistry } from '../../app/settings/server'; export const createFederationServiceSettings = async (): Promise => { - await settingsRegistry.addGroup('Federation Service', async function () { + await settingsRegistry.addGroup('Federation', async function () { await this.add('Federation_Service_Enabled', false, { type: 'boolean', - i18nLabel: 'Federation_Service_Enabled', - i18nDescription: 'Federation_Service_Enabled_Description', public: true, + enterprise: true, + modules: ['federation'], + invalidValue: false, alert: 'Federation_Service_Alert', }); - await this.add('Federation_Service_Matrix_Port', 3000, { - type: 'int', - i18nLabel: 'Federation_Service_Matrix_Port', - i18nDescription: 'Federation_Service_Matrix_Port_Description', - public: true, - alert: 'Federation_Service_Matrix_Port_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_Key', '', { + await this.add('Federation_Service_Matrix_Signing_Version', '0', { type: 'string', - i18nLabel: 'Federation_Service_Matrix_Signing_Key', - i18nDescription: 'Federation_Service_Matrix_Signing_Key_Description', public: false, + readonly: true, + enterprise: true, + modules: ['federation'], + invalidValue: '0', + }); + + // https://spec.matrix.org/v1.16/appendices/#signing-details + await this.add('Federation_Service_Matrix_Signing_Key', '', { + 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', '', { 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 032d9ee30e0ce..91c8f403e2815 100644 --- a/apps/meteor/server/settings/index.ts +++ b/apps/meteor/server/settings/index.ts @@ -37,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(), @@ -51,8 +53,6 @@ await Promise.all([ createDiscussionsSettings(), createEmailSettings(), createE2ESettings(), - createFederationSettings(), - createFederationServiceSettings(), createFileUploadSettings(), createGeneralSettings(), createIRCSettings(), @@ -79,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/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 8ad7ffd9c94cf..62218d28dcc67 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -59,6 +59,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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 siteUrl = await Settings.get('Site_Url'); @@ -78,7 +80,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS matrixDomain: serverHostname, version: process.env.SERVER_VERSION || '1.0', port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), - signingKey: settingsSigningKey, + signingKey: `${settingsSigningAlg} ${settingsSigningVersion} ${settingsSigningKey}`, signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', database: { uri: mongoUri, 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 1693ec0f8108f..55eea0dc4b97a 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", @@ -2155,14 +2155,14 @@ "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_Matrix_Domain": "Matrix domain", - "Federation_Service_Matrix_Domain_Description": "The domain of the Matrix server to use for federation.", - "Federation_Service_Matrix_Port": "Matrix port", - "Federation_Service_Matrix_Port_Description": "The port of the Matrix server to use for federation.", - "Federation_Service_Matrix_Port_Alert": "If you're using a DNS or a reverse proxy, you should set this to the port of the DNS handling the federation traffic. E.g. your server is running on port 3000 and you're using a DNS to handle incoming traffic from port 3000 to the DNS name rc1.server.com only. In this case, you should set this to 443.", - "Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.", - "Federation_Service_Matrix_Signing_Key": "Matrix server signing key", - "Federation_Service_Matrix_Signing_Key_Description": "The private signing key used by your Matrix server to authenticate federation requests. Format should be: algorithm version base64. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated Matrix servers and should be kept confidential.", + "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_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", 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": "状态", From 1606314b25ffd52d67e78697ecd01076e44a7512 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 21 Sep 2025 10:41:43 -0300 Subject: [PATCH 68/99] Fix unneeded federation version control --- .../ee/server/hooks/federation/index.ts | 4 +-- .../meteor/server/methods/addRoomModerator.ts | 4 +-- apps/meteor/server/methods/addRoomOwner.ts | 4 +-- .../server/services/federation/utils.ts | 30 ++----------------- .../room/hooks/BeforeFederationActions.ts | 4 +-- 5 files changed, 9 insertions(+), 37 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index b2c617c487113..19956210cddea 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -8,7 +8,6 @@ import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoom import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback'; import { beforeAddUserToRoom } from '../../../../lib/callbacks/beforeAddUserToRoom'; import { beforeChangeRoomRole } from '../../../../lib/callbacks/beforeChangeRoomRole'; -import { getFederationVersion } from '../../../../server/services/federation/utils'; import { FederationActions } from '../../../../server/services/room/hooks/BeforeFederationActions'; // callbacks.add('federation-event-example', async () => FederationMatrix.handleExample(), callbacks.priority.MEDIUM, 'federation-event-example-handler'); @@ -39,9 +38,8 @@ callbacks.add( async (message, { room, user }) => { if (FederationActions.shouldPerformFederationAction(room)) { const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); - const federationVersion = getFederationVersion(); - if (shouldBeHandledByFederation && federationVersion === 'native') { + if (shouldBeHandledByFederation) { 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 diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index 6cfec0032d8ce..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'); } diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index f13426a7013f9..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'); } diff --git a/apps/meteor/server/services/federation/utils.ts b/apps/meteor/server/services/federation/utils.ts index d36f98961b8d4..dd9f0165b626a 100644 --- a/apps/meteor/server/services/federation/utils.ts +++ b/apps/meteor/server/services/federation/utils.ts @@ -1,36 +1,10 @@ import { settings } from '../../../app/settings/server'; -// TODO: We should check if the federation service is ready instead of just -// checking if the matrix federation is enabled -export function getFederationVersion(): 'matrix' | 'native' | null { - if (settings.get('Federation_Matrix_enabled')) { - return 'matrix'; - } - - if (settings.get('Federation_Service_Enabled')) { - return 'native'; - } - - return null; -} - export function isFederationEnabled(): boolean { - if (getFederationVersion() === 'native') { - return true; - } - - return settings.get('Federation_Matrix_enabled'); -} - -export function isFederationReady(): boolean { - if (getFederationVersion() === 'native') { - return true; - } - - return settings.get('Federation_Matrix_configuration_status') === 'Valid'; + return settings.get('Federation_Service_Enabled'); } -export function throwIfFederationNotEnabledOrNotReady(): void { +export function throwIfFederationNotEnabled(): void { if (!isFederationEnabled()) { throw new Error('Federation is not enabled'); } diff --git a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts index dbc0b8eb2b44e..082edcb71781e 100644 --- a/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/room/hooks/BeforeFederationActions.ts @@ -1,7 +1,7 @@ import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings'; import type { IRoomNativeFederated, IRoom } from '@rocket.chat/core-typings'; -import { throwIfFederationNotEnabledOrNotReady } from '../../federation/utils'; +import { throwIfFederationNotEnabled } from '../../federation/utils'; export class FederationActions { public static shouldPerformFederationAction(room: IRoom): room is IRoomNativeFederated { @@ -25,6 +25,6 @@ export class FederationActions { throw new Error('Room is federated but its not native implementation'); } - throwIfFederationNotEnabledOrNotReady(); + throwIfFederationNotEnabled(); } } From c5ae11935778e5c7eaaf3cc75bc7f60f2f18ebf4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Sep 2025 15:19:52 -0300 Subject: [PATCH 69/99] chore(federation): refactor edu handling (#37013) Co-authored-by: Diego Sampaio --- .../ee/server/hooks/federation/index.ts | 25 +-------- apps/meteor/ee/server/index.ts | 6 -- apps/meteor/ee/server/startup/federation.ts | 12 ++++ .../modules/listeners/listeners.module.ts | 55 ++++++++----------- .../federation-matrix/src/FederationMatrix.ts | 39 +++++++------ .../federation-matrix/src/events/edu.ts | 7 ++- packages/core-services/src/events/Events.ts | 7 +-- .../src/types/IFederationMatrixService.ts | 1 + 8 files changed, 64 insertions(+), 88 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index 19956210cddea..cd93fa5b7e790 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,8 +1,7 @@ -import { api, FederationMatrix } from '@rocket.chat/core-services'; +import { FederationMatrix } from '@rocket.chat/core-services'; import { isEditedMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { MatrixBridgedRoom, Rooms } from '@rocket.chat/models'; -import notifications from '../../../../app/notifications/server/lib/Notifications'; import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback'; @@ -16,8 +15,6 @@ import { FederationActions } from '../../../../server/services/room/hooks/Before callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, originalMemberList: members, options }) => { if (FederationActions.shouldPerformFederationAction(room)) { const federatedRoomId = options?.federatedRoomId; - // TODO: move this to the hooks folder - setupTypingEventListenerForRoom(room._id); if (!federatedRoomId) { // if room if exists, we don't want to create it again @@ -225,23 +222,3 @@ callbacks.add( callbacks.priority.HIGH, 'federation-matrix-after-create-direct-room', ); - -// TODO: THIS IS NOT READY FOR PRODUCTION! IMPOSSIBLE TO ADD ONE LISTENER PER ROOM! -const setupTypingEventListenerForRoom = (roomId: string): void => { - notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { - if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { - void api.broadcast('user.typing', { - user: { username }, - isTyping: activity.includes('user-typing'), - roomId, - }); - } - }); -}; - -export const setupInternalEDUEventListeners = async () => { - const federatedRooms = await Rooms.findFederatedRooms({ projection: { _id: 1 } }).toArray(); - for (const room of federatedRooms) { - setupTypingEventListenerForRoom(room._id); - } -}; diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 960b3b2f66245..e3604bbb36141 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -14,12 +14,6 @@ import './local-services/ldap/service'; import './methods/getReadReceipts'; import './patches'; import './hooks/federation'; -import { License } from '@rocket.chat/license'; export * from './apps/startup'; export { registerEEBroker } from './startup'; - -await License.onLicense('federation', async () => { - const { setupInternalEDUEventListeners } = await import('./hooks/federation'); - await setupInternalEDUEventListeners(); -}); diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts index c98cf7b7c1371..12b4df1a8a370 100644 --- a/apps/meteor/ee/server/startup/federation.ts +++ b/apps/meteor/ee/server/startup/federation.ts @@ -5,6 +5,7 @@ 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'); @@ -27,6 +28,17 @@ export const startFederationService = async (): Promise => { 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); diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 49574d964da8e..017108bdec0be 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -156,7 +156,28 @@ export class ListenersModule { notifications.notifyRoom(rid, 'videoconf', callId); }); - service.onEvent('presence.status', ({ user }) => this.handlePresence({ user }, notifications)); + service.onEvent('presence.status', ({ user }) => { + const { _id, username, name, status, statusText, roles } = user; + if (!status || !username) { + return; + } + + notifications.notifyUserInThisInstance(_id, 'userData', { + type: 'updated', + id: _id, + diff: { + status, + ...(statusText && { statusText }), + }, + unset: {}, + }); + + notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); + + if (_id) { + notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); + } + }); service.onEvent('user.updateCustomStatus', (userStatus) => { notifications.notifyLoggedInThisInstance('updateCustomUserStatus', { @@ -164,12 +185,10 @@ export class ListenersModule { }); }); - service.onEvent('federation-matrix.user.typing', ({ isTyping, roomId, username }) => { - notifications.notifyRoom(roomId, 'user-activity', username, isTyping ? ['user-typing'] : []); + service.onEvent('user.activity', ({ isTyping, roomId, user }) => { + notifications.notifyRoom(roomId, 'user-activity', user, isTyping ? ['user-typing'] : []); }); - service.onEvent('federation-matrix.user.presence.status', ({ user }) => this.handlePresence({ user }, notifications)); - service.onEvent('watch.messages', async ({ message }) => { if (!message.rid) { return; @@ -496,30 +515,4 @@ export class ListenersModule { notifications.streamRoomMessage.emit(roomId, acknowledgeMessage); }); } - - private handlePresence( - { user }: { user: Pick }, - notifications: NotificationsModule, - ): void { - const { _id, username, name, status, statusText, roles } = user; - if (!status || !username) { - return; - } - - notifications.notifyUserInThisInstance(_id, 'userData', { - type: 'updated', - id: _id, - diff: { - status, - ...(statusText && { statusText }), - }, - unset: {}, - }); - - notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); - - if (_id) { - notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); - } - } } diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 62218d28dcc67..ba67579a44e83 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -116,24 +116,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS instance.homeserverServices = getAllServices(); MatrixMediaService.setHomeserverServices(instance.homeserverServices); instance.buildMatrixHTTPRoutes(); - instance.onEvent('user.typing', async ({ isTyping, roomId, user: { username } }): Promise => { - if (!roomId || !username) { - return; - } - const externalRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); - if (!externalRoomId) { - return; - } - const localUser = await Users.findOneByUsername(username, { projection: { _id: 1 } }); - if (!localUser) { - return; - } - const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); - if (!externalUserId) { - return; - } - void instance.homeserverServices.edu.sendTypingNotification(externalRoomId, externalUserId, isTyping); - }); + instance.onEvent( 'presence.status', async ({ user }: { user: Pick }): Promise => { @@ -1004,4 +987,24 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel); } + + async notifyUserTyping(rid: string, user: string, isTyping: boolean) { + if (!rid || !user) { + return; + } + const externalRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); + if (!externalRoomId) { + return; + } + const localUser = await Users.findOneByUsername(user, { projection: { _id: 1 } }); + if (!localUser) { + return; + } + const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); + if (!externalUserId) { + return; + } + + void this.homeserverServices.edu.sendTypingNotification(externalRoomId, externalUserId, isTyping); + } } diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index 3fab701075a3c..47ab6ae31e7db 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -28,8 +28,8 @@ export const edus = async (emitter: Emitter) => { return; } - void api.broadcast('federation-matrix.user.typing', { - username: user.username, + void api.broadcast('user.activity', { + user: user.username, isTyping: data.typing, roomId: matrixRoom, }); @@ -71,8 +71,9 @@ export const edus = async (emitter: Emitter) => { ); const { _id, username, statusText, roles, name } = user; - void api.broadcast('federation-matrix.user.presence.status', { + void api.broadcast('presence.status', { user: { status, _id, username, statusText, roles, name }, + previousStatus: undefined, }); logger.debug(`Updated presence for user ${matrixUser.uid} to ${status} from Matrix federation`); } catch (error) { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 01e25a327a959..f6eb3d7291412 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -151,12 +151,7 @@ export type EventSignatures = { scope?: string; }): void; 'user.updateCustomStatus'(userStatus: Omit): void; - 'user.typing'(data: { user: Partial; isTyping: boolean; roomId: string }): void; - 'federation-matrix.user.typing'(data: { username: string; isTyping: boolean; roomId: string }): void; - 'federation-matrix.user.presence.status'(data: { - user: Pick; - previousStatus?: UserStatus; - }): 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/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index a1ea231df3535..9be6df23fb8bb 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -35,4 +35,5 @@ export interface IFederationMatrixService { role: 'moderator' | 'owner' | 'leader' | 'user', ): Promise; inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: Pick): Promise; + notifyUserTyping(rid: string, user: string, isTyping: boolean): Promise; } From 6703c214c0750ee58cb67eaf21943c7868e80f33 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Sep 2025 16:15:25 -0300 Subject: [PATCH 70/99] fix(federation): empty thread id leads to wrong reply (#37023) --- .../federation-matrix/src/FederationMatrix.ts | 8 ++-- .../federation-matrix/src/events/message.ts | 43 ++++++++++--------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index ba67579a44e83..b404f5e010bc9 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; -import type { PresenceState } from '@hs/core'; +import type { FileMessageType, PresenceState } from '@hs/core'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; @@ -30,9 +30,7 @@ import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; import { MatrixMediaService } from './services/MatrixMediaService'; -type MatrixFileTypes = 'm.image' | 'm.video' | 'm.audio' | 'm.file'; - -export const fileTypes: Record = { +export const fileTypes: Record = { image: 'm.image', video: 'm.video', audio: 'm.audio', @@ -396,7 +394,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - private getMatrixMessageType(mimeType?: string): MatrixFileTypes { + private getMatrixMessageType(mimeType?: string): FileMessageType { const mainType = mimeType?.split('/')[0]; if (!mainType) { return fileTypes.file; diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index aa99a2b79c81d..3f3d61a226c0d 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,4 +1,6 @@ +import type { FileMessageType, MessageType } from '@hs/core'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; +import type { EventID } from '@hs/room'; import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser, IRoom } from '@rocket.chat/core-typings'; @@ -108,7 +110,7 @@ async function getRoomAndEnsureSubscription(matrixRoomId: string, user: IUser): return room; } -async function getThreadMessageId(threadRootEventId: string): Promise<{ tmid: string; tshow: boolean } | undefined> { +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); @@ -122,7 +124,7 @@ async function getThreadMessageId(threadRootEventId: string): Promise<{ tmid: st async function handleMediaMessage( // TODO improve typing content: any, - msgtype: string, + msgtype: MessageType, messageBody: string, user: IUser, room: IRoom, @@ -218,8 +220,7 @@ async function handleMediaMessage( export function message(emitter: Emitter, serverName: string) { emitter.on('homeserver.matrix.message', async (data) => { try { - // TODO remove type casting - const content = data.content as any; + const { content } = data; const msgtype = content?.msgtype; const messageBody = content?.body?.toString(); @@ -238,24 +239,26 @@ export function message(emitter: Emitter, serverName: return; } - const replyToRelation = content?.['m.relates_to']; - const threadRelation = content?.['m.relates_to']; - const isThreadMessage = threadRelation?.rel_type === 'm.thread'; - const isQuoteMessage = replyToRelation?.['m.in_reply_to']?.event_id && !replyToRelation?.is_falling_back; - const threadRootEventId = isThreadMessage ? threadRelation.event_id : undefined; - const thread = await getThreadMessageId(threadRootEventId); + const relation = content['m.relates_to']; - const isMediaMessage = Object.values(fileTypes).includes(msgtype); + const isThreadMessage = relation && relation.rel_type === 'm.thread'; + const threadRootEventId = isThreadMessage && relation.event_id; - const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; - if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { + const quoteMessageEventId = relation && 'm.in_reply_to' in relation && relation['m.in_reply_to']?.event_id; + + const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined; + + const isMediaMessage = Object.values(fileTypes).includes(msgtype as FileMessageType); + + 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(data.content['m.relates_to'].event_id); + const originalMessage = await Messages.findOneByFederationId(relation.event_id); if (!originalMessage) { - logger.error('Original message not found for edit:', data.content['m.relates_to'].event_id); + logger.error('Original message not found for edit:', relation.event_id); return; } - if (originalMessage.federation?.eventId !== data.content['m.relates_to'].event_id) { + if (originalMessage.federation?.eventId !== relation.event_id) { return; } if (originalMessage.msg === data.content['m.new_content']?.body) { @@ -263,7 +266,7 @@ export function message(emitter: Emitter, serverName: return; } - if (isQuoteMessage && room.name) { + if (quoteMessageEventId && room.name) { const messageToReplyToUrl = await MeteorService.getMessageURLToReplyTo( room.t as string, room._id, @@ -305,10 +308,10 @@ export function message(emitter: Emitter, serverName: return; } - if (isQuoteMessage && room.name) { - const originalMessage = await Messages.findOneByFederationId(replyToRelation?.['m.in_reply_to']?.event_id); + if (quoteMessageEventId && room.name) { + const originalMessage = await Messages.findOneByFederationId(quoteMessageEventId); if (!originalMessage) { - logger.error('Original message not found for quote:', replyToRelation?.['m.in_reply_to']?.event_id); + 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); From 06a16aebf5c9f8c9bbc9f2fd294c83d4a8d8997b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 21 Sep 2025 14:38:41 -0300 Subject: [PATCH 71/99] Fix isFederationEnabledMiddleware inverted logic --- .../src/api/middlewares/isFederationEnabled.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts b/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts index ae60ab600e11b..692130bbdc938 100644 --- a/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts +++ b/ee/packages/federation-matrix/src/api/middlewares/isFederationEnabled.ts @@ -2,7 +2,7 @@ import { Settings } from '@rocket.chat/core-services'; import { createMiddleware } from 'hono/factory'; export const isFederationEnabledMiddleware = createMiddleware(async (c, next) => { - if (await Settings.get('Federation_Enabled')) { + if (!(await Settings.get('Federation_Service_Enabled'))) { return c.json({ error: 'Federation is not enabled' }, 403); } return next(); From eb0f19f9f986f77b19ac68f6912a5c11ac995deb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 21 Sep 2025 18:36:40 -0300 Subject: [PATCH 72/99] Fix typecast of processInvite --- ee/packages/federation-matrix/src/api/_matrix/invite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index e44d548eaf196..25a1549d94859 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -356,7 +356,7 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); void startJoiningRoom({ - inviteEvent: inviteEvent as PersistentEventBase, // TODO: change the processInvite return type + inviteEvent, user: ourUser, room, state, From 0382d4780fbceeeca54fcf583c01c00900bf7eca Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 23 Sep 2025 00:35:47 -0300 Subject: [PATCH 73/99] feat: add origin to room name to prevent cross-server conflicts (#37034) --- .../federation-matrix/src/api/_matrix/invite.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 25a1549d94859..3768317d2019f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -238,13 +238,6 @@ async function joinRoom({ const internalMappedRoomId = await MatrixBridgedRoom.getLocalRoomId(inviteEvent.roomId); if (!internalMappedRoomId) { - let roomName: string; - try { - roomName = matrixRoom.name || ''; - } catch (error) { - roomName = inviteEvent.roomId.split(':')[0].replace('!', '') || 'Unnamed Room'; - } - let roomType: 'c' | 'p' | 'd'; if (isDM) { @@ -272,6 +265,10 @@ async function joinRoom({ throw new Error('inviteeUser user not found'); } + // TODO: Rethink room name on DMs + // get the other user than ourself + const roomName = matrixRoom.name === senderUser.username ? inviteeUser.username : senderUser.username; + ourRoom = await Room.create(senderUserId, { type: roomType, name: roomName, @@ -285,6 +282,9 @@ async function joinRoom({ }, }); } else { + const roomFname = `${matrixRoom.name}:${matrixRoom.origin}`; + const roomName = inviteEvent.roomId.replace('!', '').replace(':', '_'); + ourRoom = await Room.create(senderUserId, { type: roomType, name: roomName, @@ -294,6 +294,7 @@ async function joinRoom({ }, extraData: { federated: true, + fname: roomFname, }, }); } From 9f75559561cea336b6ebeff8987626f1b5253465 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 23 Sep 2025 00:55:15 -0300 Subject: [PATCH 74/99] chore: remove matrix bridged collections (#37035) Co-authored-by: Guilherme Gazzo --- .../server/functions/saveRoomName.ts | 2 +- .../server/functions/saveRoomTopic.ts | 2 +- .../hooks/propagateDiscussionMetadata.ts | 2 +- .../lib/server/functions/createDirectRoom.ts | 7 +- .../app/lib/server/functions/createRoom.ts | 19 +- .../app/lib/server/functions/deleteMessage.ts | 2 +- .../server/functions/setUserActiveStatus.ts | 19 +- .../app/lib/server/methods/sendMessage.ts | 8 +- .../ee/server/hooks/federation/index.ts | 78 ++-- .../lib/engagementDashboard/messages.ts | 2 +- apps/meteor/lib/callbacks.ts | 8 +- apps/meteor/server/models.ts | 4 - .../rocket-chat/adapters/Statistics.ts | 6 +- .../federation-matrix/src/FederationMatrix.ts | 359 +++++++----------- .../src/api/_matrix/invite.ts | 70 +--- .../federation-matrix/src/events/edu.ts | 33 +- .../federation-matrix/src/events/invite.ts | 20 +- .../federation-matrix/src/events/member.ts | 28 +- .../federation-matrix/src/events/message.ts | 108 +----- .../federation-matrix/src/events/room.ts | 27 +- .../src/helpers/identifiers.ts | 81 ---- .../src/types/IFederationMatrixService.ts | 16 +- packages/core-typings/src/IRoom.ts | 2 + packages/core-typings/src/IUser.ts | 7 +- .../src/federation/IMatrixBridgedRoom.ts | 6 - .../src/federation/IMatrixBridgedUser.ts | 8 - packages/core-typings/src/federation/index.ts | 3 - packages/model-typings/src/index.ts | 2 - .../src/models/IMatrixBridgedRoomModel.ts | 11 - .../src/models/IMatrixBridgedUserModel.ts | 12 - .../model-typings/src/models/IRoomsModel.ts | 3 +- packages/models/src/index.ts | 8 - packages/models/src/modelClasses.ts | 2 - .../models/src/models/MatrixBridgedRoom.ts | 45 --- .../models/src/models/MatrixBridgedUser.ts | 60 --- packages/models/src/models/Rooms.ts | 15 +- packages/models/src/models/Subscriptions.ts | 10 +- 37 files changed, 315 insertions(+), 780 deletions(-) delete mode 100644 ee/packages/federation-matrix/src/helpers/identifiers.ts delete mode 100644 packages/core-typings/src/federation/IMatrixBridgedRoom.ts delete mode 100644 packages/core-typings/src/federation/IMatrixBridgedUser.ts delete mode 100644 packages/model-typings/src/models/IMatrixBridgedRoomModel.ts delete mode 100644 packages/model-typings/src/models/IMatrixBridgedUserModel.ts delete mode 100644 packages/models/src/models/MatrixBridgedRoom.ts delete mode 100644 packages/models/src/models/MatrixBridgedUser.ts 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 947f38e6df444..0a8a83be49c1e 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts @@ -28,6 +28,6 @@ export const saveRoomTopic = async ( if (update && sendMessage) { await Message.saveSystemMessage('room_changed_topic', rid, roomTopic || '', user); } - await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, userId: user._id }); + 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/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index b5207256c3a7a..c1867de9feb03 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -46,6 +46,7 @@ export async function createDirectRoom( options: { creator?: string; subscriptionExtra?: ISubscriptionExtraData; + federatedRoomId?: string; }, ): Promise { const maxUsers = settings.get('DirectMesssage_maxUsers') || 1; @@ -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 d7da31646d451..3302425fbc58f 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -132,7 +132,18 @@ export const createRoom = async ( rid: string; } > => { - const { teamId, ...extraData } = roomExtraData || ({} as IRoom); + const { teamId, ...optionalExtraData } = roomExtraData || ({} as IRoom); + + const extraData = { + ...optionalExtraData, + ...(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, @@ -191,12 +202,6 @@ export const createRoom = async ( fname: name, _updatedAt: now, ...extraData, - ...(extraData.federated && { - federated: true, - federation: { - version: 1, - }, - }), name: isDiscussion ? name : await getValidRoomName(name.trim(), undefined), t: type, msgs: 0, 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 { - if (FederationActions.shouldPerformFederationAction(room)) { - const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':'); - - if (shouldBeHandledByFederation) { - 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); - } + 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, 'federation-v2-after-room-message-sent', ); + callbacks.add( 'afterDeleteMessage', - async (message: IMessage, room) => { + async (message: IMessage, { room, user }) => { if (!message.federation?.eventId) { return; } - const isFromExternalUser = message.u?.username?.includes(':'); - if (isFromExternalUser) { + // 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(message); + await FederationMatrix.deleteMessage(room.federation.mrid, message, user.federation.mui); } }, callbacks.priority.MEDIUM, @@ -146,7 +150,7 @@ afterLeaveRoomCallback.add( afterRemoveFromRoomCallback.add( async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise => { if (FederationActions.shouldPerformFederationAction(room)) { - await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved); + await FederationMatrix.kickUser(room, data.removedUser, data.userWhoRemoved); } }, callbacks.priority.HIGH, @@ -155,9 +159,9 @@ afterRemoveFromRoomCallback.add( callbacks.add( 'afterRoomNameChange', - async ({ room, name, userId }) => { + async ({ room, name, user }) => { if (FederationActions.shouldPerformFederationAction(room)) { - await FederationMatrix.updateRoomName(room._id, name, userId); + await FederationMatrix.updateRoomName(room._id, name, user); } }, callbacks.priority.HIGH, @@ -166,9 +170,9 @@ callbacks.add( callbacks.add( 'afterRoomTopicChange', - async (_, { room, topic, userId }) => { + async (_, { room, topic, user }) => { if (FederationActions.shouldPerformFederationAction(room)) { - await FederationMatrix.updateRoomTopic(room._id, topic, userId); + await FederationMatrix.updateRoomTopic(room, topic, user); } }, callbacks.priority.HIGH, @@ -182,9 +186,8 @@ callbacks.add( if (!isEditedMessage(message)) { return; } - FederationActions.shouldPerformFederationAction(room); - await FederationMatrix.updateMessage(message._id, message.msg, message.u); + await FederationMatrix.updateMessage(room, message); } }, callbacks.priority.HIGH, @@ -194,7 +197,7 @@ callbacks.add( 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._id, params.fromUserId, params.userId, params.role); + await FederationMatrix.addUserRoleRoomScoped(params.room, params.fromUserId, params.userId, params.role); } }, callbacks.priority.HIGH, @@ -214,7 +217,14 @@ callbacks.add( callbacks.add( 'afterCreateDirectRoom', - async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }): Promise => { + 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); } 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/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 8aca3095551f0..bade59f363c41 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; @@ -205,7 +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, userId }: { room: IRoom; topic: string; userId: IUser['_id'] }) => void; + 'afterRoomTopicChange': (params: undefined, { room, topic, user }: { room: IRoom; topic: string; user: IUser }) => void; }; export type Hook = 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/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/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index b404f5e010bc9..94dda8c04bf7e 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -5,12 +5,19 @@ import { ConfigService, createFederationContainer, getAllServices } from '@hs/fe import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; -import { isDeletedMessage, isMessageFromMatrixFederation, isQuoteAttachment, UserStatus } from '@rocket.chat/core-typings'; -import type { MessageQuoteAttachment, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +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 { Router } from '@rocket.chat/http-router'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; +import { Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; import emojione from 'emojione'; import { getWellKnownRoutes } from './api/.well-known/server'; @@ -26,7 +33,6 @@ import { isFederationDomainAllowedMiddleware } from './api/middlewares/isFederat import { isFederationEnabledMiddleware } from './api/middlewares/isFederationEnabled'; import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; import { registerEvents } from './events'; -import { saveExternalUserIdForLocalUser } from './helpers/identifiers'; import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; import { MatrixMediaService } from './services/MatrixMediaService'; @@ -121,12 +127,12 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS if (!user.username || !user.status) { return; } - const localUser = await Users.findOneByUsername(user.username, { projection: { _id: 1 } }); + const localUser = await Users.findOneByUsername(user.username, { projection: { _id: 1, federated: 1, federation: 1 } }); if (!localUser) { return; } - const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); - if (!externalUserId) { + + if (!isUserNativeFederated(localUser)) { return; } @@ -141,7 +147,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS void instance.homeserverServices.edu.sendPresenceUpdateToRooms( [ { - user_id: externalUserId, + user_id: localUser.federation.mui, presence: statusMap[user.status] || 'offline', }, ], @@ -187,13 +193,13 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return this.httpRoutes; } - async createRoom(room: IRoom, owner: IUser, members: string[]): Promise { + 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'); - return; + throw new Error('Homeserver services not available'); } - if (!(room.t === 'c' || room.t === 'p')) { + if (room.t !== 'c' && room.t !== 'p') { throw new Error('Room is not a public or private room'); } @@ -206,25 +212,13 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug('Matrix room created:', matrixRoomResult); - await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, this.serverName); - - await saveExternalUserIdForLocalUser(owner, matrixUserId); + await Rooms.setAsFederated(room._id, { mrid: matrixRoomResult.room_id, origin: this.serverName }); for await (const member of members) { if (member === owner.username) { continue; } - try { - // TODO: Check if it is external user - split domain etc - const localUserId = await Users.findOneByUsername(member); - if (localUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(localUserId._id, member, false, this.serverName); - // continue; - } - } catch (error) { - this.logger.error('Error creating or updating bridged user:', error); - } // We are not generating bridged users for members outside of the current workspace // They will be created when the invite is accepted @@ -232,8 +226,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } this.logger.debug('Room creation completed successfully', room._id); + + return matrixRoomResult; } catch (error) { - console.log(error); this.logger.error('Failed to create room:', error); throw error; } @@ -256,21 +251,23 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS continue; } - const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`; - const existingUser = await Users.findOneByUsername(username); if (existingUser) { - const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id); - if (!existingBridge) { - const remoteDomain = externalUserId.split(':')[1] || this.serverName; - await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain); - } + // TODO review: DM + // const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id); // TODO review: DM + // if (!existingBridge) { + // const remoteDomain = externalUserId.split(':')[1] || this.serverName; + // await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain); + // } continue; } + // TODO: there is not need to check if the username includes ':' or '@', we should just use the username as is + const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`; this.logger.debug('Creating federated user locally', { externalUserId, username }); - const remoteDomain = externalUserId.split(':')[1] || this.serverName; + const remoteDomain = externalUserId.split(':')[1]; + const localName = username.split(':')[0]?.replace('@', '') || username; const newUser = { @@ -284,13 +281,14 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS federated: true, federation: { version: 1, + mui: externalUserId, + origin: remoteDomain, }, createdAt: new Date(), _updatedAt: new Date(), }; const { insertedId } = await Users.insertOne(newUser); - await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, remoteDomain); this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId }); } @@ -313,13 +311,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw new Error('Creator not found in members list'); } - const matrixUserId = `@${creator.username}:${this.serverName}`; - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(creator._id); - if (!existingMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(creator._id, matrixUserId, true, this.serverName); - } - - const actualMatrixUserId = existingMatrixUserId || matrixUserId; + const actualMatrixUserId = `@${creator.username}:${this.serverName}`; let matrixRoomResult: { room_id: string; event_id?: string }; if (members.length === 2) { @@ -348,13 +340,16 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS matrixRoomResult = await this.homeserverServices.room.createRoom(actualMatrixUserId, roomName, 'invite'); } - const mapping = await MatrixBridgedRoom.getLocalRoomId(matrixRoomResult.room_id); - if (!mapping) { - await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, matrixRoomResult.room_id, this.serverName); - } + // 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; + if (typeof member !== 'string' && member._id === creatorId) { + continue; + } try { let memberMatrixUserId: string; @@ -367,15 +362,31 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS memberMatrixUserId = member.username.startsWith('@') ? member.username : `@${member.username}`; memberId = member._id; } else { - memberMatrixUserId = `@${member.username}:${this.serverName}`; - memberId = member._id; + continue; } if (memberId) { - const existingMemberMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(memberId); - + const existingMemberMatrixUserId = await Users.findOne({ 'federation.mui': memberId }); if (!existingMemberMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(memberId, memberMatrixUserId, true, this.serverName); + 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); } } @@ -386,7 +397,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.error('Error creating or updating bridged user for DM:', error); } } - await Rooms.setAsFederated(room._id); + 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); @@ -531,31 +545,20 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS ); } - async sendMessage(message: IMessage, room: IRoom, user: IUser): Promise { + async sendMessage(message: IMessage, room: IRoomNativeFederated, user: IUser): Promise { try { - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); - if (!matrixRoomId) { - throw new Error(`No Matrix room mapping found for room ${room._id}`); - } - - const matrixUserId = `@${user.username}:${this.serverName}`; - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); - if (!existingMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, this.serverName); - } - if (!this.homeserverServices) { this.logger.warn('Homeserver services not available, skipping message send'); return; } - const actualMatrixUserId = existingMatrixUserId || matrixUserId; + 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, matrixRoomId, actualMatrixUserId, this.serverName); + result = await this.handleFileMessage(message, room.federation.mrid, userMui, this.serverName); } else { - result = await this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName); + result = await this.handleTextMessage(message, room.federation.mrid, userMui, this.serverName); } if (!result) { @@ -611,30 +614,25 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS }; } - async deleteMessage(message: IMessage): Promise { + async deleteMessage(matrixRoomId: string, message: IMessage, uid: string): Promise { try { if (!isMessageFromMatrixFederation(message) || isDeletedMessage(message)) { return; } - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); - if (!matrixRoomId) { - throw new Error(`No Matrix room mapping found for room ${message.rid}`); - } - const matrixUserId = `@${message.u.username}:${this.serverName}`; - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(message.u._id); - if (!existingMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(message.u._id, matrixUserId, true, this.serverName); - } 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}`); } - const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId as EventID, matrixUserId); + + // 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) { @@ -643,13 +641,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - async inviteUsersToRoom(room: IRoom, usersUserName: string[], inviter: IUser): Promise { + async inviteUsersToRoom(room: IRoomNativeFederated, usersUserName: string[], inviter: IUser): Promise { try { - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); - if (!matrixRoomId) { - throw new Error(`No Matrix room mapping found for room ${room._id}`); - } - const inviterUserId = `@${inviter.username}:${this.serverName}`; await Promise.all( @@ -664,11 +657,11 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } - await this.homeserverServices.invite.inviteUserToRoom(username, matrixRoomId, inviterUserId); + await this.homeserverServices.invite.inviteUserToRoom(username, room.federation.mrid, inviterUserId); }), ); } catch (error) { - this.logger.error('Failed to invite an user to Matrix:', error); + this.logger.error({ msg: 'Failed to invite an user to Matrix:', err: error }); throw error; } } @@ -680,8 +673,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw new Error(`Message ${messageId} not found`); } - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); - if (!matrixRoomId) { + const room = await Rooms.findOneById(message.rid); + if (!room || !isRoomNativeFederated(room)) { throw new Error(`No Matrix room mapping found for room ${message.rid}`); } @@ -692,13 +685,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const reactionKey = emojione.shortnameToUnicode(reaction); - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); - if (!existingMatrixUserId) { - this.logger.error(`No Matrix user ID mapping found for user ${user._id}`); - return; - } + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; - const eventId = await this.homeserverServices.message.sendReaction(matrixRoomId, matrixEventId, reactionKey, existingMatrixUserId); + const eventId = await this.homeserverServices.message.sendReaction(room.federation.mrid, matrixEventId, reactionKey, userMui); await Messages.setFederationReactionEventId(user.username || '', messageId, reaction, eventId); @@ -723,18 +712,15 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return; } - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); - if (!matrixRoomId) { + 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 existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); - if (!existingMatrixUserId) { - this.logger.error(`No Matrix user ID mapping found for user ${user._id}`); - return; - } + + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; const reactionData = oldMessage.reactions?.[reaction]; if (!reactionData?.federationReactionEventIds) { @@ -747,10 +733,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } const redactionEventId = await this.homeserverServices.message.unsetReaction( - matrixRoomId, + room.federation.mrid, eventId as EventID, reactionKey, - existingMatrixUserId, + userMui, ); if (!redactionEventId) { this.logger.warn('No reaction event found to remove in Matrix'); @@ -783,121 +769,81 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS async leaveRoom(roomId: string, user: IUser): Promise { try { const room = await Rooms.findOneById(roomId); - if (!room?.federated) { + if (!room || !isRoomNativeFederated(room)) { this.logger.debug(`Room ${roomId} is not federated, skipping leave operation`); return; } - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); - if (!matrixRoomId) { - this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping leave`); - return; - } - - const matrixUserId = `@${user.username}:${this.serverName}`; - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); - - if (!existingMatrixUserId) { - // User might not have been bridged yet if they never sent a message - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, this.serverName); - } - if (!this.homeserverServices) { this.logger.warn('Homeserver services not available, skipping room leave'); return; } - const actualMatrixUserId = existingMatrixUserId || matrixUserId; + const actualMatrixUserId = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; - await this.homeserverServices.room.leaveRoom(matrixRoomId, actualMatrixUserId); + await this.homeserverServices.room.leaveRoom(room.federation.mrid, actualMatrixUserId); - this.logger.info(`User ${user.username} left Matrix room ${matrixRoomId} successfully`); + 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(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise { - try { - const room = await Rooms.findOneById(roomId); - if (!room?.federated) { - this.logger.debug(`Room ${roomId} is not federated, skipping kick operation`); - return; - } - - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId); - if (!matrixRoomId) { - this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping kick`); - return; - } - - const kickedMatrixUserId = `@${removedUser.username}:${this.serverName}`; - const existingKickedMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(removedUser._id); - if (!existingKickedMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, this.serverName); - } - const actualKickedMatrixUserId = existingKickedMatrixUserId || kickedMatrixUserId; + async kickUser(room: IRoomNativeFederated, removedUser: IUser, userWhoRemoved: IUser): Promise { + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping user kick'); + return; + } - const senderMatrixUserId = `@${userWhoRemoved.username}:${this.serverName}`; - const existingSenderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userWhoRemoved._id); - if (!existingSenderMatrixUserId) { - await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, this.serverName); - } - const actualSenderMatrixUserId = existingSenderMatrixUserId || senderMatrixUserId; + try { + const actualKickedMatrixUserId = isUserNativeFederated(removedUser) + ? removedUser.federation.mui + : `@${removedUser.username}:${this.serverName}`; - if (!this.homeserverServices) { - this.logger.warn('Homeserver services not available, skipping user kick'); - return; - } + const actualSenderMatrixUserId = isUserNativeFederated(userWhoRemoved) + ? userWhoRemoved.federation.mui + : `@${userWhoRemoved.username}:${this.serverName}`; await this.homeserverServices.room.kickUser( - matrixRoomId, + room.federation.mrid, actualKickedMatrixUserId, actualSenderMatrixUserId, `Kicked by ${userWhoRemoved.username}`, ); - this.logger.info(`User ${removedUser.username} was kicked from Matrix room ${matrixRoomId} 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(messageId: string, newContent: string, sender: IUser): Promise { + async updateMessage(room: IRoomNativeFederated, message: IMessage): Promise { try { - const message = await Messages.findOneById(messageId); - if (!message) { - throw new Error(`Message ${messageId} not found`); - } - - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); - if (!matrixRoomId) { - 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}`); + throw new Error(`No Matrix event ID mapping found for message ${message._id}`); } - const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(sender._id); - if (!existingMatrixUserId) { - this.logger.error(`No Matrix user ID mapping found for user ${sender._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: newContent, - externalRoomId: matrixRoomId, + message: message.msg, + externalRoomId: room.federation.mrid, homeServerDomain: this.serverName, }); const eventId = await this.homeserverServices.message.updateMessage( - matrixRoomId, - newContent, + room.federation.mrid, + message.msg, parsedMessage, - existingMatrixUserId, + userMui, matrixEventId, ); @@ -908,47 +854,36 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - async updateRoomName(rid: string, displayName: string, senderId: string): Promise { + 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); - if (!matrixRoomId) { + const room = await Rooms.findOneById(rid); + if (!room || !isRoomNativeFederated(room)) { throw new Error(`No Matrix room mapping found for room ${rid}`); } - const userId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); - if (!userId) { - throw new Error(`No Matrix user ID mapping found for user ${senderId}`); - } + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; - await this.homeserverServices.room.updateRoomName(matrixRoomId, displayName, userId); + await this.homeserverServices.room.updateRoomName(room.federation.mrid, displayName, userMui); } - async updateRoomTopic(rid: string, topic: string, senderId: string): Promise { + 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 matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); - if (!matrixRoomId) { - throw new Error(`No Matrix room mapping found for room ${rid}`); - } + const userMui = isUserNativeFederated(user) ? user.federation.mui : `@${user.username}:${this.serverName}`; - const userId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); - if (!userId) { - throw new Error(`No Matrix user ID mapping found for user ${senderId}`); - } - - await this.homeserverServices.room.setRoomTopic(matrixRoomId, userId, topic); + await this.homeserverServices.room.setRoomTopic(room.federation.mrid, userMui, topic); } async addUserRoleRoomScoped( - rid: string, + room: IRoomNativeFederated, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user', @@ -962,20 +897,17 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw new Error('Leader role is not supported'); } - const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); - if (!matrixRoomId) { - throw new Error(`No Matrix room mapping found for room ${rid}`); - } - - const matrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); - if (!matrixUserId) { - throw new Error(`No Matrix user ID mapping found for user ${userId}`); + 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 senderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(senderId); - if (!senderMatrixUserId) { - throw new Error(`No Matrix user ID mapping found for user ${senderId}`); + 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') { @@ -983,26 +915,27 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } else if (role === 'moderator') { powerLevel = 50; } - await this.homeserverServices.room.setPowerLevelForUser(matrixRoomId, senderMatrixUserId, matrixUserId, powerLevel); + await this.homeserverServices.room.setPowerLevelForUser(room.federation.mrid, senderMui, userMui, powerLevel); } async notifyUserTyping(rid: string, user: string, isTyping: boolean) { if (!rid || !user) { return; } - const externalRoomId = await MatrixBridgedRoom.getExternalRoomId(rid); - if (!externalRoomId) { + const room = await Rooms.findOneById(rid); + if (!room || !isRoomNativeFederated(room)) { return; } - const localUser = await Users.findOneByUsername(user, { projection: { _id: 1 } }); + const localUser = await Users.findOneByUsername>(user, { + projection: { _id: 1, username: 1, federation: 1, federated: 1 }, + }); + if (!localUser) { return; } - const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(localUser._id); - if (!externalUserId) { - return; - } - void this.homeserverServices.edu.sendTypingNotification(externalRoomId, externalUserId, isTyping); + 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/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 3768317d2019f..180197361e1db 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -3,7 +3,7 @@ import type { PduMembershipEventContent, PersistentEventBase, RoomVersion } from import { Room } from '@rocket.chat/core-services'; import type { IUser, UserStatus } from '@rocket.chat/core-typings'; import { Router } from '@rocket.chat/http-router'; -import { MatrixBridgedRoom, MatrixBridgedUser, Rooms, Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; const EventBaseSchema = { @@ -176,32 +176,14 @@ async function joinRoom({ } // need both the sender and the participating user to exist in the room - const internalSenderUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(inviteEvent.sender); - - let senderUserId: string; - - if (!internalSenderUserId) { - // create locally - // what we were using previously - /* - public getStorageRepresentation(): Readonly { - return { - _id: this.internalId, - username: this.internalReference.username || '', - type: this.internalReference.type, - status: this.internalReference.status, - active: this.internalReference.active, - roles: this.internalReference.roles, - name: this.internalReference.name, - requirePasswordChange: this.internalReference.requirePasswordChange, - createdAt: new Date(), - _updatedAt: new Date(), - federated: this.isRemote(), - }; - } - */ + // TODO implement on model + const senderUser = await Users.findOne({ 'federation.mui': inviteEvent.sender }, { projection: { _id: 1 } }); + + let senderUserId = senderUser?._id; - const user = { + // create locally + if (!senderUser) { + const createdUser = await Users.insertOne({ // let the _id auto generate we deal with usernames username: inviteEvent.sender, type: 'user', @@ -213,31 +195,25 @@ async function joinRoom({ federated: true, federation: { version: 1, + mui: inviteEvent.sender, + origin: matrixRoom.origin, }, createdAt: new Date(), _updatedAt: new Date(), - }; - - const createdUser = await Users.insertOne(user); + }); senderUserId = createdUser.insertedId; + } - await MatrixBridgedUser.createOrUpdateByLocalId(senderUserId, inviteEvent.sender, true, matrixRoom.origin); - } else { - // already got the mapped sender - const user = await Users.findOneById(internalSenderUserId); - if (!user) { - throw new Error('user not found although should have as it is in mapping not processing invite'); - } - - senderUserId = user._id; + if (!senderUserId) { + throw new Error('Sender user ID not found'); } let internalRoomId: string; - const internalMappedRoomId = await MatrixBridgedRoom.getLocalRoomId(inviteEvent.roomId); + const internalMappedRoom = await Rooms.findOne({ 'federation.mrid': inviteEvent.roomId }); - if (!internalMappedRoomId) { + if (!internalMappedRoom) { let roomType: 'c' | 'p' | 'd'; if (isDM) { @@ -301,19 +277,15 @@ async function joinRoom({ internalRoomId = ourRoom._id; } else { - const room = await Rooms.findOneById(internalMappedRoomId); - if (!room) { - throw new Error('room not found although should have as it is in mapping not processing invite'); - } - - internalRoomId = room._id; + internalRoomId = internalMappedRoom._id; } await Room.addUserToRoom(internalRoomId, { _id: user._id }, { _id: senderUserId, username: inviteEvent.sender }); - if (isDM) { - await MatrixBridgedRoom.createOrUpdateByLocalRoomId(internalRoomId, inviteEvent.roomId, matrixRoom.origin); - } + // TODO is this needed? + // if (isDM) { + // await MatrixBridgedRoom.createOrUpdateByLocalRoomId(internalRoomId, inviteEvent.roomId, matrixRoom.origin); + // } } async function startJoiningRoom(...opts: Parameters) { diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index 47ab6ae31e7db..b03ae9937a096 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -3,35 +3,29 @@ import { api } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:edu'); export const edus = async (emitter: Emitter) => { emitter.on('homeserver.matrix.typing', async (data) => { try { - const matrixRoom = await MatrixBridgedRoom.getLocalRoomId(data.room_id); + 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 MatrixBridgedUser.findOne({ mui: data.user_id }); - if (!matrixUser) { + 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; } - const user = await Users.findOneById(matrixUser.uid, { projection: { _id: 1, username: 1 } }); - if (!user || !user.username) { - logger.debug(`User not found for uid: ${matrixUser.uid}`); - return; - } - void api.broadcast('user.activity', { - user: user.username, + user: matrixUser.username, isTyping: data.typing, - roomId: matrixRoom, + roomId: matrixRoom._id, }); } catch (error) { logger.error('Error handling Matrix typing event:', error); @@ -40,18 +34,11 @@ export const edus = async (emitter: Emitter) => { emitter.on('homeserver.matrix.presence', async (data) => { try { - const matrixUser = await MatrixBridgedUser.findOne({ mui: data.user_id }); + const matrixUser = await Users.findOne({ 'federation.mui': data.user_id }); if (!matrixUser) { logger.debug(`No bridged user found for Matrix user_id: ${data.user_id}`); return; } - const user = await Users.findOneById(matrixUser.uid, { - projection: { _id: 1, username: 1, statusText: 1, roles: 1, name: 1, status: 1 }, - }); - if (!user) { - logger.debug(`User not found for uid: ${matrixUser.uid}`); - return; - } const statusMap = { online: UserStatus.ONLINE, @@ -61,7 +48,7 @@ export const edus = async (emitter: Emitter) => { const status = statusMap[data.presence] || UserStatus.OFFLINE; await Users.updateOne( - { _id: user._id }, + { _id: matrixUser._id }, { $set: { status, @@ -70,12 +57,12 @@ export const edus = async (emitter: Emitter) => { }, ); - const { _id, username, statusText, roles, name } = user; + 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.uid} to ${status} from Matrix federation`); + 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/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index 225b475f9bbf6..7629ba9a510dd 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -2,11 +2,11 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { Room } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; export function invite(emitter: Emitter) { emitter.on('homeserver.matrix.accept-invite', async (data) => { - const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); + const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); if (!room) { console.warn(`No bridged room found for room_id: ${data.room_id}`); return; @@ -15,10 +15,15 @@ export function invite(emitter: Emitter) { const internalUsername = data.sender; const localUser = await Users.findOneByUsername(internalUsername); if (localUser) { - await Room.addUserToRoom(room.rid, 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', @@ -32,19 +37,16 @@ export function invite(emitter: Emitter) { federated: true, federation: { version: 1, + mui: data.sender, + origin: serverName, }, }); - const serverName = data.sender.split(':')[1] || 'unknown'; - const bridgedUser = await MatrixBridgedUser.findOne({ mui: data.sender }); - if (!bridgedUser) { - await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, data.sender, true, serverName); - } const user = await Users.findOneById(insertedId); if (!user) { console.warn(`User with ID ${insertedId} not found after insertion`); return; } - await Room.addUserToRoom(room.rid, user); + await Room.addUserToRoom(room._id, user); }); } diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index 91f930dd77ab0..e5ce0e557b5c5 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -2,7 +2,7 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { Room } from '@rocket.chat/core-services'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; -import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:member'); @@ -15,44 +15,34 @@ export function member(emitter: Emitter) { return; } - const room = await MatrixBridgedRoom.findOne({ mri: data.room_id }); + 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 affectedMatrixUser = await MatrixBridgedUser.findOne({ mui: data.state_key }); - if (!affectedMatrixUser) { - logger.warn(`No bridged user found for Matrix user_id: ${data.state_key}`); - return; - } - - const affectedUser = await Users.findOneById(affectedMatrixUser.uid); + const affectedUser = await Users.findOne({ 'federation.mui': data.state_key }); if (!affectedUser) { - logger.error(`No Rocket.Chat user found for bridged user: ${affectedMatrixUser.uid}`); + 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.rid, affectedUser); - logger.info(`User ${affectedUser.username} left room ${room.rid} via Matrix federation`); + await Room.removeUserFromRoom(room._id, affectedUser); + logger.info(`User ${affectedUser.username} left room ${room._id} via Matrix federation`); } else { // Kick - find who kicked - const kickerMatrixUser = await MatrixBridgedUser.findOne({ mui: data.sender }); - let kickerUser = null; - if (kickerMatrixUser) { - kickerUser = await Users.findOneById(kickerMatrixUser.uid); - } + const kickerUser = await Users.findOne({ 'federation.mui': data.sender }); - await Room.removeUserFromRoom(room.rid, affectedUser, { + 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.rid} by ${data.sender} via Matrix federation.${reasonText}`); + logger.info(`User ${affectedUser.username} was kicked from room ${room._id} by ${data.sender} via Matrix federation.${reasonText}`); } } 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 index 3f3d61a226c0d..243680d566310 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -2,11 +2,10 @@ import type { FileMessageType, MessageType } from '@hs/core'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; -import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser, IRoom } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; -import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; +import { Users, Rooms, Messages } from '@rocket.chat/models'; import { fileTypes } from '../FederationMatrix'; import { toInternalMessageFormat, toInternalQuoteMessageFormat } from '../helpers/message.parsers'; @@ -14,102 +13,6 @@ import { MatrixMediaService } from '../services/MatrixMediaService'; const logger = new Logger('federation-matrix:message'); -async function getOrCreateFederatedUser(matrixUserId: string): Promise { - const [userPart, domain] = matrixUserId.split(':'); - if (!userPart || !domain) { - logger.error('Invalid Matrix sender ID format:', matrixUserId); - return null; - } - const username = userPart.substring(1); - - const user = await Users.findOneByUsername(matrixUserId); - if (user) { - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, false, domain); - return user; - } - - logger.info('Creating new federated user:', { username: matrixUserId, externalId: matrixUserId }); - - const userData = { - username: matrixUserId, - name: username, // TODO: Fetch display name from Matrix profile - type: 'user', - status: UserStatus.ONLINE, - active: true, - roles: ['user'], - requirePasswordChange: false, - federated: true, - federation: { - version: 1, - }, - createdAt: new Date(), - _updatedAt: new Date(), - }; - - const { insertedId } = await Users.insertOne(userData); - - await MatrixBridgedUser.createOrUpdateByLocalId( - insertedId, - matrixUserId, - true, // isRemote = true for external Matrix users - domain, - ); - - const newUser = await Users.findOneById(insertedId); - if (!newUser) { - logger.error('Failed to create user:', matrixUserId); - return null; - } - - logger.info('Successfully created federated user:', { userId: newUser._id, username }); - - return newUser; -} - -async function getRoomAndEnsureSubscription(matrixRoomId: string, user: IUser): Promise { - const internalRoomId = await MatrixBridgedRoom.getLocalRoomId(matrixRoomId); - if (!internalRoomId) { - logger.error('Room not found in bridge mapping:', matrixRoomId); - // TODO: Handle room creation for unknown federated rooms - return null; - } - - const room = await Rooms.findOneById(internalRoomId); - if (!room) { - logger.error('Room not found:', internalRoomId); - return null; - } - - if (!room.federated) { - logger.error('Room is not marked as federated:', { roomId: room._id, matrixRoomId }); - // TODO: Should we update the room to be federated? - } - - const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); - - if (existingSubscription) { - return room; - } - - logger.info('Creating subscription for federated user in room:', { userId: user._id, roomId: room._id }); - - const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { - ts: new Date(), - open: false, - alert: false, - unread: 0, - userMentions: 0, - groupMentions: 0, - }); - - if (insertedId) { - logger.debug('Successfully created subscription:', insertedId); - // TODO: Import and use notifyOnSubscriptionChangedById if needed - } - - return room; -} - async function getThreadMessageId(threadRootEventId: EventID): Promise<{ tmid: string; tshow: boolean } | undefined> { const threadRootMessage = await Messages.findOneByFederationId(threadRootEventId); if (!threadRootMessage) { @@ -229,14 +132,15 @@ export function message(emitter: Emitter, serverName: return; } - const user = await getOrCreateFederatedUser(data.sender); + // at this point we know for sure the user already exists + const user = await Users.findOne({ 'federation.mui': data.sender }); if (!user) { - return; + throw new Error(`User not found for sender: ${data.sender}`); } - const room = await getRoomAndEnsureSubscription(data.room_id, user); + const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); if (!room) { - return; + throw new Error(`No mapped room found for room_id: ${data.room_id}`); } const relation = content['m.relates_to']; diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index a083485f99751..7e09c60053df4 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -1,64 +1,59 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { Room } from '@rocket.chat/core-services'; import type { Emitter } from '@rocket.chat/emitter'; -import { MatrixBridgedRoom, MatrixBridgedUser, Rooms } from '@rocket.chat/models'; +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 MatrixBridgedRoom.getLocalRoomId(roomId); + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); if (!localRoomId) { throw new Error('mapped room not found'); } - const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); if (!localUserId) { throw new Error('mapped user not found'); } - await Room.saveRoomName(localRoomId, localUserId, name); + 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 MatrixBridgedRoom.getLocalRoomId(roomId); + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); if (!localRoomId) { throw new Error('mapped room not found'); } - const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); if (!localUserId) { throw new Error('mapped user not found'); } - await Room.saveRoomTopic(localRoomId, topic, { _id: localUserId, username: userId }); + 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 MatrixBridgedRoom.getLocalRoomId(roomId); + const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } }); if (!localRoomId) { throw new Error('mapped room not found'); } - const localRoom = await Rooms.findOneById(localRoomId); - if (!localRoom) { - throw new Error('mapped room object not found'); - } - - const localUserId = await MatrixBridgedUser.getLocalUserIdByExternalId(userId); + const localUserId = await Users.findOne({ 'federation.mui': userId }, { projection: { _id: 1 } }); if (!localUserId) { throw new Error('mapped user not found'); } - const localSenderId = await MatrixBridgedUser.getLocalUserIdByExternalId(senderId); + const localSenderId = await Users.findOne({ 'federation.mui': senderId }, { projection: { _id: 1 } }); if (!localSenderId) { throw new Error('mapped user not found'); } - await Room.addUserRoleRoomScoped(localSenderId, localUserId, localRoomId, role); + await Room.addUserRoleRoomScoped(localSenderId._id, localUserId._id, localRoomId._id, role); }); } diff --git a/ee/packages/federation-matrix/src/helpers/identifiers.ts b/ee/packages/federation-matrix/src/helpers/identifiers.ts deleted file mode 100644 index 781d3831194cc..0000000000000 --- a/ee/packages/federation-matrix/src/helpers/identifiers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { IUser, UserStatus } from '@rocket.chat/core-typings'; -import { MatrixBridgedUser, Users } from '@rocket.chat/models'; - -export const convertExternalUserIdToInternalUsername = (externalUserId: string): string => externalUserId.replace(/@/g, ''); - -export const getLocalUsernameForMatrixUserIdToSave = (matrixUserId: string): string => matrixUserId; // TODO: decide on whether to keep @ or not - -export const getLocalNameForMatrixUserIdToSave = (matrixUserId: string): string => - matrixUserId.split(':').shift()?.replace(/@/g, '') as string; - -// can have none if in case of local user -export const getExternalUserIdForLocalUserToSave = (user: IUser): string | undefined => - user.federated ? /* remote user, already has @ according to the function above */ (user.username as string) : undefined; - -export async function getLocalUserForExternalUserId(externalUserId: string): Promise { - const localUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(externalUserId); - if (!localUserId) { - return null; - } - - const user = await Users.findOneById(localUserId); - if (!user) { - throw new Error('user not found although should have as it is in mapping not processing invite'); - } - - return user; -} - -export async function getExternalUserIdForLocalUser(user: IUser): Promise { - const externalUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id); - return externalUserId; -} - -export async function saveExternalUserIdForLocalUser(user: IUser, externalUserId: string): Promise { - const matrixDomain = externalUserId.split(':')[1]; - await MatrixBridgedUser.createOrUpdateByLocalId(user._id, externalUserId, true, matrixDomain); -} - -export async function saveLocalUserForExternalUserId(externalUserId: string, origin: string): Promise { - /* - * using from ------- - public getStorageRepresentation(): Readonly { - return { - _id: this.internalId, - username: this.internalReference.username || '', - type: this.internalReference.type, - status: this.internalReference.status, - active: this.internalReference.active, - roles: this.internalReference.roles, - name: this.internalReference.name, - requirePasswordChange: this.internalReference.requirePasswordChange, - createdAt: new Date(), - _updatedAt: new Date(), - federated: this.isRemote(), - }; - } - */ - - const user = { - // let the _id auto generate we deal with usernames - username: getLocalUsernameForMatrixUserIdToSave(externalUserId), - type: 'user', - status: 'online' as UserStatus, - active: true, - roles: ['user'], - name: getLocalNameForMatrixUserIdToSave(externalUserId), - requirePasswordChange: false, - federated: true, - federation: { - version: 1, - }, - createdAt: new Date(), - _updatedAt: new Date(), - }; - - const { insertedId } = await Users.insertOne(user); - - await MatrixBridgedUser.createOrUpdateByLocalId(insertedId, externalUserId, true, origin); - - return insertedId; -} diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 9be6df23fb8bb..be59a924d09df 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,4 +1,4 @@ -import type { AtLeast, IMessage, IRoomFederated, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoomFederated, IRoomNativeFederated, IUser } from '@rocket.chat/core-typings'; import type { Router } from '@rocket.chat/http-router'; export interface IRouteContext { @@ -15,21 +15,21 @@ export interface IFederationMatrixService { matrix: Router<'/_matrix'>; wellKnown: Router<'/.well-known'>; }; - createRoom(room: IRoomFederated, owner: IUser, members: string[]): Promise; + createRoom(room: IRoomFederated, owner: IUser, members: string[]): Promise<{ room_id: string; event_id: string }>; ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise; createDirectMessageRoom(room: IRoomFederated, members: IUser[], creatorId: IUser['_id']): Promise; sendMessage(message: IMessage, room: IRoomFederated, user: IUser): Promise; - deleteMessage(message: IMessage): 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(rid: IRoomFederated['_id'], removedUser: IUser, userWhoRemoved: IUser): Promise; - updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; - updateRoomName(rid: IRoomFederated['_id'], name: string, sender: string): Promise; - updateRoomTopic(rid: IRoomFederated['_id'], topic: string, sender: string): 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( - rid: IRoomFederated['_id'], + room: IRoomNativeFederated, senderId: string, userId: string, role: 'moderator' | 'owner' | 'leader' | 'user', diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 7f3ca6348c233..629ac5ee1e124 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -114,6 +114,8 @@ export interface IRoomFederated extends IRoom { export interface IRoomNativeFederated extends IRoomFederated { federation: { version: number; + // Matrix's room ID. Example: !XqJXqZxXqJXq:matrix.org + mrid: string; }; } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index bab6a3e40b461..bd76e342b7a5d 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -228,6 +228,8 @@ export interface IUser extends IRocketChatRecord { // @deprecated federation?: { 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; + 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/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 68faceaba46aa..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/IMatrixBridgedRoomModel'; -export * from './models/IMatrixBridgedUserModel'; export * from './models/ICalendarEventModel'; export * from './models/IOmnichannelServiceLevelAgreementsModel'; export * from './models/IAppLogsModel'; diff --git a/packages/model-typings/src/models/IMatrixBridgedRoomModel.ts b/packages/model-typings/src/models/IMatrixBridgedRoomModel.ts deleted file mode 100644 index 538e05336a1ad..0000000000000 --- a/packages/model-typings/src/models/IMatrixBridgedRoomModel.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/IMatrixBridgedUserModel.ts b/packages/model-typings/src/models/IMatrixBridgedUserModel.ts deleted file mode 100644 index 02ad87604fee0..0000000000000 --- a/packages/model-typings/src/models/IMatrixBridgedUserModel.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/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/models/src/index.ts b/packages/models/src/index.ts index 2aa03d57b725e..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, @@ -119,8 +117,6 @@ import { TeamRaw, UsersRaw, UsersSessionsRaw, - MatrixBridgedUserRaw, - MatrixBridgedRoomRaw, } from './modelClasses'; import { proxify, registerModel } from './proxify'; @@ -219,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', @@ -264,8 +258,6 @@ export function registerServiceModels(db: Db, trash?: Collection new LivechatRoomsRaw(db)); registerModel('IUploadsModel', () => new UploadsRaw(db)); registerModel('ILivechatVisitorsModel', () => new LivechatVisitorsRaw(db)); - registerModel('IMatrixBridgedUserModel', () => new MatrixBridgedUserRaw(db)); - registerModel('IMatrixBridgedRoomModel', () => new MatrixBridgedRoomRaw(db)); } if (!dbWatchersDisabled) { 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/Rooms.ts b/packages/models/src/models/Rooms.ts index 8529cf674d8c1..43c01e46a057c 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -667,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 { @@ -1969,12 +1969,6 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { _id: (await this.insertOne(room)).insertedId, _updatedAt: new Date(), ...room, - ...(room.federated && { - federated: true, - federation: { - version: 1, - }, - }), }; return newRoom; @@ -2219,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 dba1abc564daa..dbd72d644cf11 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -2077,18 +2077,10 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri 'room.federated': true, }, }, - { - $lookup: { - from: 'rocketchat_matrix_bridged_rooms', - localField: 'rid', - foreignField: 'rid', - as: 'matrixRoom', - }, - }, { $project: { _id: '$rid', - externalRoomId: { $arrayElemAt: ['$matrixRoom.mri', 0] }, + externalRoomId: { $arrayElemAt: ['$room.federation.mrid', 0] }, }, }, ]); From ebf0659d4f8c9d740bdd2057c737dbe7b0be12ad Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 23 Sep 2025 01:19:18 -0300 Subject: [PATCH 75/99] fix: fixes reviews of chore/federation-backup (#37016) Co-authored-by: Guilherme Gazzo --- .../ee/server/hooks/federation/index.ts | 7 +- apps/meteor/lib/callbacks.ts | 4 +- .../server/services/messages/service.ts | 5 +- ee/apps/federation-service/src/config.ts | 20 ---- .../federation-matrix/src/FederationMatrix.ts | 40 ++++---- .../src/api/_matrix/invite.ts | 8 +- .../src/api/_matrix/profiles.ts | 7 +- .../src/api/_matrix/rooms.ts | 7 +- .../src/api/_matrix/send-join.ts | 6 +- .../federation-matrix/src/events/message.ts | 95 +++++++++---------- .../src/services/MatrixMediaService.ts | 4 +- .../src/types/IFederationMatrixService.ts | 11 +-- .../core-typings/src/IMessage/IMessage.ts | 3 +- 13 files changed, 94 insertions(+), 123 deletions(-) diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index fb8e423948831..67ad46bcd15e9 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -17,7 +17,8 @@ callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, origi const federatedRoomId = options?.federatedRoomId; if (!federatedRoomId) { - // if room if exists, we don't want to create it again + // 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 @@ -51,7 +52,7 @@ callbacks.add( } }, callbacks.priority.HIGH, - 'federation-v2-after-room-message-sent', + 'native-federation-after-room-message-sent', ); callbacks.add( @@ -84,7 +85,7 @@ callbacks.add( if (FederationActions.shouldPerformFederationAction(room)) { await FederationMatrix.inviteUsersToRoom( room, - invitees.map((invitee) => (typeof invitee === 'string' ? invitee : (invitee.username as string))), + invitees.map((invitee) => (typeof invitee === 'string' ? invitee : invitee.username)).filter((v) => v != null), inviter, ); } diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index bade59f363c41..e9c004e146ccd 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -80,10 +80,10 @@ interface EventLikeCallbackSignatures { ) => void; 'beforeCreateDirectRoom': (members: IUser[], room: IRoom) => void; 'federation.beforeCreateDirectMessage': (members: IUser[]) => void; - 'afterSetReaction': (message: IMessage, parems: { user: IUser; reaction: string; shouldReact: boolean; room: IRoom }) => void; + 'afterSetReaction': (message: IMessage, params: { user: IUser; reaction: string; shouldReact: boolean; room: IRoom }) => void; 'afterUnsetReaction': ( message: IMessage, - parems: { user: IUser; reaction: string; shouldReact: boolean; oldMessage: IMessage; room: IRoom }, + 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; diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 85938ba38fb0f..ec28bc8ef6fe8 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -108,7 +108,10 @@ export class MessageService extends ServiceClassInternal implements IMessageServ rid, msg, ...thread, - federation: { eventId: federation_event_id }, + federation: { + eventId: federation_event_id, + version: 1, + }, ...(file && { file }), ...(files && { files }), ...(attachments && { attachments }), diff --git a/ee/apps/federation-service/src/config.ts b/ee/apps/federation-service/src/config.ts index 0469553f8ab56..b4afd9b8be330 100644 --- a/ee/apps/federation-service/src/config.ts +++ b/ee/apps/federation-service/src/config.ts @@ -1,23 +1,3 @@ -export type Config = { - port: number; - host: string; - routePrefix: string; - rocketchatUrl: string; - authMode: 'jwt' | 'api-key' | 'internal'; - logLevel: 'debug' | 'info' | 'warn' | 'error'; - nodeEnv: 'development' | 'production' | 'test'; -}; - -export function isRunningMs(): boolean { - return !!process.env.TRANSPORTER?.match(/^(?:nats|TCP)/); -} - export const config = { port: parseInt(process.env.FEDERATION_SERVICE_PORT || '3030'), - host: process.env.FEDERATION_SERVICE_HOST || '0.0.0.0', - routePrefix: process.env.FEDERATION_ROUTE_PREFIX || '/_matrix', - rocketchatUrl: process.env.ROCKETCHAT_URL || '', - authMode: (process.env.FEDERATION_AUTH_MODE as any) || 'jwt', - logLevel: (process.env.LOG_LEVEL as any) || 'info', - nodeEnv: (process.env.NODE_ENV as any) || 'development', }; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 94dda8c04bf7e..74c76ba7d87d3 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -85,7 +85,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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 || './rc1.signing.key', + signingKeyPath: process.env.CONFIG_FOLDER || './rocketchat.signing.key', database: { uri: mongoUri, name: dbName, @@ -243,8 +243,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS if (typeof member === 'string') { username = member; + } else if (typeof member.username === 'string') { + username = member.username; } else { - username = member.username as string; + continue; } if (!username.includes(':') && !username.includes('@')) { @@ -428,22 +430,26 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } try { - // TODO: Handle multiple files - const file = message.files[0]; - const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain); - - const msgtype = this.getMatrixMessageType(file.type); - const fileContent = { - body: file.name, - msgtype, - url: mxcUri, - info: { - mimetype: file.type, - size: file.size, - }, - }; + let lastEventId: { eventId: string } | null = null; + + for await (const file of message.files) { + const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain); + + 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 this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId); + return lastEventId; } catch (error) { this.logger.error('Failed to handle file message', { messageId: message._id, diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 180197361e1db..f4f2f22e55cdf 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -229,16 +229,14 @@ async function joinRoom({ let ourRoom: { _id: string }; if (isDM) { - const [senderUser, inviteeUser] = await Promise.all([ - Users.findOneById(senderUserId, { projection: { _id: 1, username: 1 } }), - Promise.resolve(user), - ]); + 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('inviteeUser user not found'); + throw new Error('Invitee user not found'); } // TODO: Rethink room name on DMs diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index 8cb242f444a21..ed0f1df740f38 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -421,11 +421,10 @@ export const getMatrixProfilesRoutes = (services: HomeserverServices) => { .get( '/v1/make_join/:roomId/:userId', { - // TODO: fix types here, likely import from room package - params: ajv.compile({ type: 'object' }), - query: ajv.compile({ type: 'object' }), + params: isMakeJoinParamsProps, + query: isMakeJoinQueryProps, response: { - 200: ajv.compile({ type: 'object' }), + 200: isMakeJoinResponseProps, }, tags: ['Federation'], license: ['federation'], diff --git a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts index 21bc9394b4afe..f78f1c93f959f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts @@ -188,9 +188,10 @@ export const getMatrixRoomsRoutes = (services: HomeserverServices) => { return r.name.toLowerCase().includes(filter.generic_search_term.toLowerCase()); } - if (filter.room_types) { - // TODO: implement room_types filtering - } + // 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; }) diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index 2b5b5aa0fdc28..1af9c076e38c0 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -228,10 +228,10 @@ export const getMatrixSendJoinRoutes = (services: HomeserverServices) => { return new Router('/federation').put( '/v2/send_join/:roomId/:stateKey', { - params: ajv.compile({ type: 'object' }), - body: ajv.compile({ type: 'object' }), + params: isSendJoinParamsProps, + body: isSendJoinEventProps, response: { - 200: ajv.compile({ type: 'object' }), + 200: isSendJoinResponseProps, }, tags: ['Federation'], license: ['federation'], diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 243680d566310..eb1a166f68a6b 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,8 +1,8 @@ -import type { FileMessageType, MessageType } from '@hs/core'; +import type { FileMessageType, MessageType, FileMessageContent } from '@hs/core'; import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { EventID } from '@hs/room'; import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services'; -import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IUser, IRoom, FileAttachmentProps } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; import { Users, Rooms, Messages } from '@rocket.chat/models'; @@ -25,13 +25,13 @@ async function getThreadMessageId(threadRootEventId: EventID): Promise<{ tmid: s } async function handleMediaMessage( - // TODO improve typing - content: any, + url: string, + fileInfo: FileMessageContent['info'], msgtype: MessageType, messageBody: string, user: IUser, room: IRoom, - eventId: string, + eventId: EventID, tmid?: string, ): Promise<{ fromId: string; @@ -39,26 +39,23 @@ async function handleMediaMessage( msg: string; federation_event_id: string; tmid?: string; - file: any; - files: any[]; - attachments: any[]; + attachments: [FileAttachmentProps]; }> { - const fileInfo = content.info; - const mimeType = fileInfo.mimetype; + const mimeType = fileInfo?.mimetype; const fileName = messageBody; - const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, { + const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(url, { name: messageBody, - size: fileInfo.size, + size: fileInfo?.size, type: mimeType, roomId: room._id, userId: user._id, }); let fileExtension = ''; - if (fileName && fileName.includes('.')) { + if (fileName?.includes('.')) { fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; - } else if (mimeType && mimeType.includes('/')) { + } else if (mimeType?.includes('/')) { fileExtension = mimeType.split('/')[1] || ''; if (fileExtension === 'jpeg') { fileExtension = 'jpg'; @@ -67,55 +64,50 @@ async function handleMediaMessage( const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`; - // TODO improve typing - const attachment: any = { + let attachment: FileAttachmentProps = { title: fileName, type: 'file', title_link: fileUrl, title_link_download: true, + description: '', }; if (msgtype === 'm.image') { - attachment.image_url = fileUrl; - attachment.image_type = mimeType; - attachment.image_size = fileInfo.size || 0; - attachment.description = ''; - if (fileInfo.w && fileInfo.h) { - attachment.image_dimensions = { - width: fileInfo.w, - height: fileInfo.h, - }; - } + 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.video_url = fileUrl; - attachment.video_type = mimeType; - attachment.video_size = fileInfo.size || 0; - attachment.description = ''; + attachment = { + ...attachment, + video_url: fileUrl, + video_type: mimeType, + video_size: fileInfo?.size || 0, + }; } else if (msgtype === 'm.audio') { - attachment.audio_url = fileUrl; - attachment.audio_type = mimeType; - attachment.audio_size = fileInfo.size || 0; - attachment.description = ''; - } else { - attachment.description = ''; + attachment = { + ...attachment, + audio_url: fileUrl, + audio_type: mimeType, + audio_size: fileInfo?.size || 0, + }; } - const fileData = { - _id: fileRefId, - name: fileName, - type: mimeType, - size: fileInfo.size || 0, - format: fileExtension, - }; - return { fromId: user._id, rid: room._id, msg: '', federation_event_id: eventId, tmid, - file: fileData, - files: [fileData], attachments: [attachment], }; } @@ -124,8 +116,8 @@ export function message(emitter: Emitter, serverName: emitter.on('homeserver.matrix.message', async (data) => { try { const { content } = data; - const msgtype = content?.msgtype; - const messageBody = content?.body?.toString(); + const { msgtype } = content; + const messageBody = content.body.toString(); if (!messageBody && !msgtype) { logger.debug('No message content found in event'); @@ -152,8 +144,6 @@ export function message(emitter: Emitter, serverName: const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined; - const isMediaMessage = Object.values(fileTypes).includes(msgtype as FileMessageType); - 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'); @@ -236,8 +226,9 @@ export function message(emitter: Emitter, serverName: return; } - if (isMediaMessage && content?.url) { - const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, thread?.tmid); + 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.event_id, thread?.tmid); await Message.saveMessageFromFederation(result); } else { const formatted = toInternalMessageFormat({ diff --git a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts index 6c7aed293e421..273674d8ab53e 100644 --- a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts +++ b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts @@ -88,8 +88,8 @@ export class MatrixMediaService { mxcUri: string, metadata: { name: string; - size: number; - type: string; + size?: number; + type?: string; messageId?: string; roomId?: string; userId?: string; diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index be59a924d09df..28bf89012b7b5 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,15 +1,6 @@ import type { IMessage, IRoomFederated, IRoomNativeFederated, IUser } from '@rocket.chat/core-typings'; import type { Router } from '@rocket.chat/http-router'; -export interface IRouteContext { - params: any; - query: any; - body: any; - headers: Record; - setStatus: (code: number) => void; - setHeader: (key: string, value: string) => void; -} - export interface IFederationMatrixService { getAllRoutes(): { matrix: Router<'/_matrix'>; @@ -34,6 +25,6 @@ export interface IFederationMatrixService { userId: string, role: 'moderator' | 'owner' | 'leader' | 'user', ): Promise; - inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: Pick): Promise; + inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: IUser): Promise; notifyUserTyping(rid: string, user: string, isTyping: boolean): Promise; } diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index ae265f26d77ab..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" */ @@ -282,7 +283,7 @@ export interface IFederatedMessage extends IMessage { export interface INativeFederatedMessage extends IMessage { federation: { - version: `${number}`; + version: number; eventId: string; }; } From 26cfc6e5fa0ceb6724cb0aeac9c76d988b7ac1fb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 23 Sep 2025 10:31:58 -0300 Subject: [PATCH 76/99] chore(federation): add setting to control EDUs (#37036) --- .../server/settings/federation-service.ts | 18 +++++++++++++++ .../federation-matrix/src/FederationMatrix.ts | 20 ++++++++++++++-- .../federation-matrix/src/events/edu.ts | 23 +++++++++++++++++-- .../federation-matrix/src/events/index.ts | 8 +++++-- packages/i18n/src/locales/en.i18n.json | 6 +++++ 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index a117c9d96a61d..797aa75cd395e 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -53,5 +53,23 @@ export const createFederationServiceSettings = async (): Promise => { 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/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 74c76ba7d87d3..cf9f095b3364d 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -56,6 +56,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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(); @@ -73,6 +77,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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); @@ -124,7 +131,11 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS instance.onEvent( 'presence.status', async ({ user }: { user: Pick }): Promise => { - if (!user.username || !user.status) { + 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 } }); @@ -136,6 +147,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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', @@ -183,7 +195,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS async created(): Promise { try { - registerEvents(this.eventHandler, this.serverName); + registerEvents(this.eventHandler, this.serverName, { typing: this.processEDUTyping, presence: this.processEDUPresence }); } catch (error) { this.logger.warn('Homeserver module not available, running in limited mode'); } @@ -925,6 +937,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } async notifyUserTyping(rid: string, user: string, isTyping: boolean) { + if (!this.processEDUTyping) { + return; + } + if (!rid || !user) { return; } diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index b03ae9937a096..5bd849698ae47 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -7,8 +7,12 @@ import { Rooms, Users } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:edu'); -export const edus = async (emitter: Emitter) => { +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) { @@ -33,10 +37,19 @@ export const edus = async (emitter: Emitter) => { }); 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 bridged user found for Matrix user_id: ${data.user_id}`); + 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; } @@ -47,6 +60,12 @@ export const edus = async (emitter: Emitter) => { }; 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 }, { diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index aa3e1eea17ca3..bead09af3fbf5 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -9,12 +9,16 @@ import { ping } from './ping'; import { reaction } from './reaction'; import { room } from './room'; -export function registerEvents(emitter: Emitter, serverName: string) { +export function registerEvents( + emitter: Emitter, + serverName: string, + eduProcessTypes: { typing: boolean; presence: boolean }, +) { ping(emitter); message(emitter, serverName); invite(emitter); reaction(emitter); member(emitter); - edus(emitter); + edus(emitter, eduProcessTypes); room(emitter); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 55eea0dc4b97a..74f39e161e7e8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2155,6 +2155,12 @@ "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_Matrix_Signing_Algorithm": "Signing Key Algorithm", "Federation_Service_Matrix_Signing_Version": "Signing Key Version", From d546bafb17f9560559a51a8ca80acc7d33299819 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 23 Sep 2025 11:40:46 -0300 Subject: [PATCH 77/99] fix: DM naming --- ee/packages/federation-matrix/src/api/_matrix/invite.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index f4f2f22e55cdf..d800ab0ae2919 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -239,13 +239,9 @@ async function joinRoom({ throw new Error('Invitee user not found'); } - // TODO: Rethink room name on DMs - // get the other user than ourself - const roomName = matrixRoom.name === senderUser.username ? inviteeUser.username : senderUser.username; - ourRoom = await Room.create(senderUserId, { type: roomType, - name: roomName, + name: inviteEvent.sender, members: [senderUser.username, inviteeUser.username], options: { federatedRoomId: inviteEvent.roomId, From 794f4b46ba1e8f9db616041a4422a436fabfe31a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 23 Sep 2025 12:49:02 -0300 Subject: [PATCH 78/99] fix direct message rc -> synapse --- .../meteor/server/methods/createDirectMessage.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 From e2c6484a94c398492564cdd60761858766bab803 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 23 Sep 2025 17:23:47 -0300 Subject: [PATCH 79/99] chore(federation): bring back domain setting (#37033) --- apps/meteor/ee/server/startup/federation.ts | 46 ++++++++++++++++++- .../server/settings/federation-service.ts | 9 ++++ .../federation-matrix/src/FederationMatrix.ts | 11 +++-- .../src/api/.well-known/server.ts | 6 ++- packages/i18n/src/locales/en.i18n.json | 3 ++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts index 12b4df1a8a370..47636b6cc2b12 100644 --- a/apps/meteor/ee/server/startup/federation.ts +++ b/apps/meteor/ee/server/startup/federation.ts @@ -10,13 +10,45 @@ 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; - return hasLicense && isEnabled; + const domain = settings.get('Federation_Service_Domain'); + const hasDomain = validateDomain(domain); + return hasLicense && isEnabled && hasDomain; }; const startService = async (): Promise => { @@ -88,4 +120,16 @@ export const startFederationService = async (): Promise => { 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/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 797aa75cd395e..8bff1f9d25f57 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -11,6 +11,15 @@ export const createFederationServiceSettings = async (): Promise => { 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, diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index cf9f095b3364d..884c3192389ab 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -70,10 +70,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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 siteUrl = await Settings.get('Site_Url'); - - const serverHostname = new URL(siteUrl).hostname; + const serverHostname = (await Settings.get('Federation_Service_Domain')).trim(); instance.serverName = serverHostname; @@ -168,6 +165,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS }, ); + instance.logger.startup(`Federation Matrix Homeserver created for domain ${instance.serverName}`); + return instance; } @@ -205,6 +204,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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'); diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts index 0ab75752a29ce..3b249f23783f8 100644 --- a/ee/packages/federation-matrix/src/api/.well-known/server.ts +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -17,6 +17,8 @@ const WellKnownServerResponseSchema = { 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; @@ -28,11 +30,11 @@ export const getWellKnownRoutes = (services: HomeserverServices) => { 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'); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 74f39e161e7e8..0943a0dbec11a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2162,6 +2162,9 @@ "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", From 5b1c09eee37fbd6b1e4cf9149cbe1a8026c4c1bf Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 23 Sep 2025 17:32:08 -0300 Subject: [PATCH 80/99] chore(federation): generate signing key at first startup (#37039) --- apps/meteor/server/settings/federation-service.ts | 6 +++++- ee/packages/federation-matrix/package.json | 1 + ee/packages/federation-matrix/src/FederationMatrix.ts | 2 ++ yarn.lock | 3 ++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 8bff1f9d25f57..2ae38944734ca 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -1,3 +1,5 @@ +import { generateEd25519RandomSecretKey } from '@rocket.chat/federation-matrix'; + import { settingsRegistry } from '../../app/settings/server'; export const createFederationServiceSettings = async (): Promise => { @@ -38,8 +40,10 @@ export const createFederationServiceSettings = async (): Promise => { invalidValue: '0', }); + const randomKey = generateEd25519RandomSecretKey().toString('base64'); + // https://spec.matrix.org/v1.16/appendices/#signing-details - await this.add('Federation_Service_Matrix_Signing_Key', '', { + await this.add('Federation_Service_Matrix_Signing_Key', randomKey, { type: 'password', public: false, enterprise: true, diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 8b1b58155ed15..d85bc95e0e615 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -34,6 +34,7 @@ "extends": "../../../package.json" }, "dependencies": { + "@hs/crypto": "workspace:^", "@hs/federation-sdk": "workspace:^", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 884c3192389ab..acd02acc6b972 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -43,6 +43,8 @@ export const fileTypes: Record = { file: 'm.file', }; +export { generateEd25519RandomSecretKey } from '@hs/crypto'; + export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; diff --git a/yarn.lock b/yarn.lock index 5e6a8ba4e237e..3b33496460a91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2965,7 +2965,7 @@ __metadata: languageName: unknown linkType: soft -"@hs/crypto@workspace:*, @hs/crypto@workspace:homeserver/packages/crypto": +"@hs/crypto@workspace:*, @hs/crypto@workspace:^, @hs/crypto@workspace:homeserver/packages/crypto": version: 0.0.0-use.local resolution: "@hs/crypto@workspace:homeserver/packages/crypto" dependencies: @@ -7732,6 +7732,7 @@ __metadata: "@babel/core": "npm:~7.26.0" "@babel/preset-env": "npm:~7.26.0" "@babel/preset-typescript": "npm:~7.26.0" + "@hs/crypto": "workspace:^" "@hs/federation-sdk": "workspace:^" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" From 3a25abb072dbf0fc13da2f38a5450e0680290b38 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 24 Sep 2025 13:39:21 -0300 Subject: [PATCH 81/99] fix(federation): DM first message not working (#37048) --- .../federation-matrix/src/api/_matrix/invite.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index d800ab0ae2919..30a28d9ff72b3 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -322,12 +322,14 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); - void startJoiningRoom({ - inviteEvent, - user: ourUser, - room, - state, - }); + setTimeout(() => { + void startJoiningRoom({ + inviteEvent, + user: ourUser, + room, + state, + }); + }, 200); return { body: { From 12c0113a150fc4e18ebf5b910cd296b7a658f098 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 24 Sep 2025 16:08:27 -0300 Subject: [PATCH 82/99] chore: Use published federation package (#37046) --- .github/workflows/ci-code-check.yml | 6 - .github/workflows/ci-test-e2e.yml | 6 - .github/workflows/ci-test-storybook.yml | 9 +- .github/workflows/ci-test-unit.yml | 5 - .github/workflows/ci.yml | 58 +- .gitignore | 1 - apps/meteor/package.json | 2 + ee/apps/federation-service/package.json | 4 +- ee/packages/federation-matrix/package.json | 8 +- .../federation-matrix/src/FederationMatrix.ts | 15 +- .../src/api/.well-known/server.ts | 2 +- .../src/api/_matrix/invite.ts | 10 +- .../src/api/_matrix/key/server.ts | 2 +- .../src/api/_matrix/media.ts | 2 +- .../src/api/_matrix/profiles.ts | 3 +- .../src/api/_matrix/rooms.ts | 2 +- .../src/api/_matrix/send-join.ts | 3 +- .../src/api/_matrix/transactions.ts | 3 +- .../src/api/_matrix/versions.ts | 2 +- .../federation-matrix/src/api/middlewares.ts | 5 +- .../federation-matrix/src/events/edu.ts | 2 +- .../federation-matrix/src/events/index.ts | 2 +- .../federation-matrix/src/events/invite.ts | 2 +- .../federation-matrix/src/events/member.ts | 2 +- .../federation-matrix/src/events/message.ts | 4 +- .../federation-matrix/src/events/ping.ts | 2 +- .../federation-matrix/src/events/reaction.ts | 2 +- .../federation-matrix/src/events/room.ts | 2 +- .../src/services/MatrixMediaService.ts | 2 +- package.json | 1 - yarn.lock | 853 ++---------------- 31 files changed, 104 insertions(+), 918 deletions(-) diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 4cf663465982b..9c903dcf5a4f7 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -31,12 +31,6 @@ jobs: swap-size-gb: 4 - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e06ba921c18e2..84ebeca360068 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -120,12 +120,6 @@ jobs: mongodb-replica-set: rs0 - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 7f293b0fed384..9052c7e39e019 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -22,14 +22,10 @@ jobs: runs-on: ubuntu-24.04 name: Test Storybook - + steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver + - name: Setup NodeJS uses: ./.github/actions/setup-node with: @@ -59,4 +55,3 @@ jobs: flags: unit verbose: true token: ${{ secrets.CODECOV_TOKEN }} - \ No newline at end of file diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 340b580e4eb4e..8cfaeef15d192 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -35,11 +35,6 @@ jobs: job_summary: true comment_on_pr: false - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d90223a61f78..569cdece7dddd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,42 +133,9 @@ jobs: "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5.0\", \"6.0\", \"7.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update - build-homeserver: - name: 📦 Build Homeserver - needs: [release-versions] - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - repository: RocketChat/homeserver - path: homeserver - - - uses: actions/cache@v4 - id: cache-homeserver - name: cache homeserver - with: - path: /tmp/homeserver - key: homeserver-${{ hashFiles('homeserver/.git/refs/heads/main') }} - - - uses: oven-sh/setup-bun@v2 - if: steps.cache-homeserver.outputs.cache-hit != 'true' - - run: | - cd homeserver - bun install - bun run build - bun run bundle:sdk - rm -rf .git - cp -r ../homeserver /tmp/homeserver - if: steps.cache-homeserver.outputs.cache-hit != 'true' - - - uses: actions/upload-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - packages-build: name: 📦 Build Packages - needs: [release-versions, notify-draft-services, build-homeserver] + needs: [release-versions, notify-draft-services] runs-on: ubuntu-24.04 steps: - name: Github Info @@ -188,13 +155,6 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - - name: Setup NodeJS uses: ./.github/actions/setup-node with: @@ -228,7 +188,7 @@ jobs: build: name: 📦 Meteor Build - coverage - needs: [release-versions, packages-build, build-homeserver] + needs: [release-versions, packages-build] runs-on: ubuntu-24.04 steps: @@ -250,12 +210,6 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} @@ -340,7 +294,7 @@ jobs: build-gh-docker-coverage: name: 🚢 Build Docker Images for Testing - needs: [build, release-versions, build-matrix-rust-bindings-for-alpine, build-homeserver] + needs: [build, release-versions, build-matrix-rust-bindings-for-alpine] runs-on: ubuntu-24.04 env: @@ -357,12 +311,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: homeserver - path: /tmp/homeserver - - run: ln -s /tmp/homeserver ${{ github.workspace }}/homeserver - # we only build and publish the actual docker images if not a PR from a fork - uses: ./.github/actions/build-docker if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' diff --git a/.gitignore b/.gitignore index 300ae136bbab6..81bca21648cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -60,5 +60,4 @@ registration.yaml storybook-static development/tempo-data/ -homeserver .env diff --git a/apps/meteor/package.json b/apps/meteor/package.json index ad3dc5acb6712..d3229d1877cdf 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -451,6 +451,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/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 9698622425be5..8baefa653817d 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -21,11 +21,11 @@ }, "dependencies": { "@hono/node-server": "^1.14.4", - "@hs/federation-sdk": "workspace:*", "@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.4", "@rocket.chat/http-router": "workspace:*", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", @@ -36,12 +36,14 @@ "polka": "^0.5.2", "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", "zod": "^3.22.0" }, "devDependencies": { "@types/bun": "latest", "@types/express": "^4.17.17", "eslint": "~8.45.0", + "pino-pretty": "^7.6.1", "typescript": "^5.3.0" }, "keywords": [ diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index d85bc95e0e615..a1578d10777ca 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -14,6 +14,7 @@ "babel-jest": "~30.0.0", "eslint": "~8.45.0", "jest": "~30.0.0", + "pino-pretty": "^7.6.1", "typescript": "~5.8.3" }, "scripts": { @@ -34,11 +35,10 @@ "extends": "../../../package.json" }, "dependencies": { - "@hs/crypto": "workspace:^", - "@hs/federation-sdk": "workspace:^", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-sdk": "0.1.4", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", @@ -50,6 +50,8 @@ "mongodb": "6.10.0", "pino": "8.21.0", "reflect-metadata": "^0.2.2", - "sanitize-html": "^2.17.0" + "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 index acd02acc6b972..9cebe72368cc4 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -1,9 +1,5 @@ import 'reflect-metadata'; -import type { FileMessageType, PresenceState } from '@hs/core'; -import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; -import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; -import type { EventID } from '@hs/room'; import { type IFederationMatrixService, ServiceClass, Settings } from '@rocket.chat/core-services'; import { isDeletedMessage, @@ -15,6 +11,15 @@ import { } 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'; @@ -43,7 +48,7 @@ export const fileTypes: Record = { file: 'm.file', }; -export { generateEd25519RandomSecretKey } from '@hs/crypto'; +export { generateEd25519RandomSecretKey } from '@rocket.chat/federation-sdk'; export class FederationMatrix extends ServiceClass implements IFederationMatrixService { protected name = 'federation-matrix'; diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts index 3b249f23783f8..c4a8920ff96f3 100644 --- a/ee/packages/federation-matrix/src/api/.well-known/server.ts +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -2,7 +2,7 @@ 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 '@hs/federation-sdk'; +import type { HomeserverServices } from '@rocket.chat/federation-sdk'; const WellKnownServerResponseSchema = { type: 'object', diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 30a28d9ff72b3..e86c6ecfa4e36 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -1,7 +1,13 @@ -import type { HomeserverServices, RoomService, StateService } from '@hs/federation-sdk'; -import type { PduMembershipEventContent, PersistentEventBase, RoomVersion } from '@hs/room'; 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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/key/server.ts b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts index a24599cce396f..a2b6ba144151f 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/key/server.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/key/server.ts @@ -1,4 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; +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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/media.ts b/ee/packages/federation-matrix/src/api/_matrix/media.ts index 650452ca09844..ac22bf76defa4 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/media.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/media.ts @@ -1,7 +1,7 @@ import crypto from 'crypto'; -import type { HomeserverServices } from '@hs/federation-sdk'; 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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts index ed0f1df740f38..57bd41fc482af 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/profiles.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/profiles.ts @@ -1,5 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; -import type { RoomVersion } from '@hs/room'; +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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts index f78f1c93f959f..ab6d05f29a58b 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/rooms.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/rooms.ts @@ -1,4 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; +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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index 1af9c076e38c0..e8ee53c4b964d 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -1,5 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; -import type { EventID } from '@hs/room'; +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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index 31f94e0bd454f..ffbdd639811e0 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -1,5 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; -import type { EventID } from '@hs/room'; +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'; diff --git a/ee/packages/federation-matrix/src/api/_matrix/versions.ts b/ee/packages/federation-matrix/src/api/_matrix/versions.ts index cac68d2f26c9e..684b3c14035aa 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/versions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/versions.ts @@ -1,4 +1,4 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; +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'; diff --git a/ee/packages/federation-matrix/src/api/middlewares.ts b/ee/packages/federation-matrix/src/api/middlewares.ts index a919cc0758103..1b1f9579d201c 100644 --- a/ee/packages/federation-matrix/src/api/middlewares.ts +++ b/ee/packages/federation-matrix/src/api/middlewares.ts @@ -1,6 +1,5 @@ -import type { EventAuthorizationService } from '@hs/federation-sdk'; -import { errCodes } from '@hs/federation-sdk'; -import type { EventID } from '@hs/room'; +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) => { diff --git a/ee/packages/federation-matrix/src/events/edu.ts b/ee/packages/federation-matrix/src/events/edu.ts index 5bd849698ae47..563ae4d404f1c 100644 --- a/ee/packages/federation-matrix/src/events/edu.ts +++ b/ee/packages/federation-matrix/src/events/edu.ts @@ -1,7 +1,7 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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'; diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index bead09af3fbf5..a8b3fb27718d1 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -1,5 +1,5 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import type { Emitter } from '@rocket.chat/emitter'; +import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; import { edus } from './edu'; import { invite } from './invite'; diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts index 7629ba9a510dd..9d0197e8731b4 100644 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ b/ee/packages/federation-matrix/src/events/invite.ts @@ -1,7 +1,7 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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 { Rooms, Users } from '@rocket.chat/models'; export function invite(emitter: Emitter) { diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index e5ce0e557b5c5..672afe91149f5 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -1,6 +1,6 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; import { Room } 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 { Rooms, Users } from '@rocket.chat/models'; diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index eb1a166f68a6b..84f266a5a16f7 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,9 +1,7 @@ -import type { FileMessageType, MessageType, FileMessageContent } from '@hs/core'; -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import type { EventID } from '@hs/room'; 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'; diff --git a/ee/packages/federation-matrix/src/events/ping.ts b/ee/packages/federation-matrix/src/events/ping.ts index 04972b23fb544..3bcd05d042431 100644 --- a/ee/packages/federation-matrix/src/events/ping.ts +++ b/ee/packages/federation-matrix/src/events/ping.ts @@ -1,5 +1,5 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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) => { diff --git a/ee/packages/federation-matrix/src/events/reaction.ts b/ee/packages/federation-matrix/src/events/reaction.ts index ee8c3393022ea..e9d9c402864f1 100644 --- a/ee/packages/federation-matrix/src/events/reaction.ts +++ b/ee/packages/federation-matrix/src/events/reaction.ts @@ -1,6 +1,6 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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'; diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index 7e09c60053df4..00deac2ad2419 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -1,6 +1,6 @@ -import type { HomeserverEventSignatures } from '@hs/federation-sdk'; 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) { diff --git a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts index 273674d8ab53e..5e4761b1d35df 100644 --- a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts +++ b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts @@ -1,6 +1,6 @@ -import type { HomeserverServices } from '@hs/federation-sdk'; 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'; diff --git a/package.json b/package.json index 4820ee6726457..365fa33ceb69a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "typescript": "~5.9.2" }, "workspaces": [ - "homeserver", "apps/*", "packages/*", "ee/apps/*", diff --git a/yarn.lock b/yarn.lock index 3b33496460a91..ee8a8957457e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1806,97 +1806,6 @@ __metadata: languageName: node linkType: hard -"@biomejs/biome@npm:^1.9.4": - version: 1.9.4 - resolution: "@biomejs/biome@npm:1.9.4" - dependencies: - "@biomejs/cli-darwin-arm64": "npm:1.9.4" - "@biomejs/cli-darwin-x64": "npm:1.9.4" - "@biomejs/cli-linux-arm64": "npm:1.9.4" - "@biomejs/cli-linux-arm64-musl": "npm:1.9.4" - "@biomejs/cli-linux-x64": "npm:1.9.4" - "@biomejs/cli-linux-x64-musl": "npm:1.9.4" - "@biomejs/cli-win32-arm64": "npm:1.9.4" - "@biomejs/cli-win32-x64": "npm:1.9.4" - dependenciesMeta: - "@biomejs/cli-darwin-arm64": - optional: true - "@biomejs/cli-darwin-x64": - optional: true - "@biomejs/cli-linux-arm64": - optional: true - "@biomejs/cli-linux-arm64-musl": - optional: true - "@biomejs/cli-linux-x64": - optional: true - "@biomejs/cli-linux-x64-musl": - optional: true - "@biomejs/cli-win32-arm64": - optional: true - "@biomejs/cli-win32-x64": - optional: true - bin: - biome: bin/biome - checksum: 10/bd8ff8fb4dc0581bd60a9b9ac28d0cd03ba17c6a1de2ab6228b7fda582079594ceee774f47e41aac2fc6d35de1637def2e32ef2e58fa24e22d1b24ef9ee5cefa - languageName: node - linkType: hard - -"@biomejs/cli-darwin-arm64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-darwin-arm64@npm:1.9.4" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@biomejs/cli-darwin-x64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-darwin-x64@npm:1.9.4" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@biomejs/cli-linux-arm64-musl@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-linux-arm64-musl@npm:1.9.4" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@biomejs/cli-linux-arm64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-linux-arm64@npm:1.9.4" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@biomejs/cli-linux-x64-musl@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-linux-x64-musl@npm:1.9.4" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@biomejs/cli-linux-x64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-linux-x64@npm:1.9.4" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@biomejs/cli-win32-arm64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-win32-arm64@npm:1.9.4" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@biomejs/cli-win32-x64@npm:1.9.4": - version: 1.9.4 - resolution: "@biomejs/cli-win32-x64@npm:1.9.4" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@blakek/curry@npm:^2.0.2": version: 2.0.2 resolution: "@blakek/curry@npm:2.0.2" @@ -1914,26 +1823,6 @@ __metadata: languageName: node linkType: hard -"@bogeychan/elysia-etag@npm:^0.0.6": - version: 0.0.6 - resolution: "@bogeychan/elysia-etag@npm:0.0.6" - peerDependencies: - elysia: ">= 1.0.22" - checksum: 10/7f21b03f0c12f66762618eb5fc4440d775e2369c3caedfa8f6eb801d6f4ebc347a18859b3745647de4d14016da30b69795397119c07b620489eebf28e875848b - languageName: node - linkType: hard - -"@bogeychan/elysia-logger@npm:^0.1.4": - version: 0.1.8 - resolution: "@bogeychan/elysia-logger@npm:0.1.8" - dependencies: - pino: "npm:^9.6.0" - peerDependencies: - elysia: ">= 1.2.10" - checksum: 10/2d81b9c7e8d094254d1bb71a48fede3e89d8da232127517645dccc1663ff0fd7259da73c09b0cb511fb003d958b51db930af481e1638a1e278ca3b1641484555 - languageName: node - linkType: hard - "@bugsnag/browser@npm:^7.20.2": version: 7.20.2 resolution: "@bugsnag/browser@npm:7.20.2" @@ -2459,22 +2348,6 @@ __metadata: languageName: node linkType: hard -"@datastructures-js/heap@npm:^4.3.3": - version: 4.3.3 - resolution: "@datastructures-js/heap@npm:4.3.3" - checksum: 10/ffcdbf2f36c354d14deee19e41e1dda61a578c17ef3bdca6777b9b1b1e63edaae9deca3d5288ba6f04dd2c82902118c67e6d6eb3a2ab833a8475466a6ffb7457 - languageName: node - linkType: hard - -"@datastructures-js/priority-queue@npm:^6.3.3": - version: 6.3.3 - resolution: "@datastructures-js/priority-queue@npm:6.3.3" - dependencies: - "@datastructures-js/heap": "npm:^4.3.3" - checksum: 10/b69dd330189d9700c6ae73b75b730e48c68413647c7715e3c521b9f050b2e096aee4ae646f380f164066112817391b78caf40e7c1db4d882a45f80e608829225 - languageName: node - linkType: hard - "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -2489,20 +2362,6 @@ __metadata: languageName: node linkType: hard -"@elysiajs/swagger@npm:^1.3.0": - version: 1.3.0 - resolution: "@elysiajs/swagger@npm:1.3.0" - dependencies: - "@scalar/themes": "npm:^0.9.52" - "@scalar/types": "npm:^0.0.12" - openapi-types: "npm:^12.1.3" - pathe: "npm:^1.1.2" - peerDependencies: - elysia: ">= 1.3.0" - checksum: 10/b065c8d20e8056f75ad04d736c3f7f17a0b851b9bbeb4df85be3ea188120e294dc3b082b877c6aff35cdd409f526f420625a464128553379494e60bea3c5e32f - languageName: node - linkType: hard - "@emnapi/core@npm:^1.4.3": version: 1.4.3 resolution: "@emnapi/core@npm:1.4.3" @@ -2952,74 +2811,6 @@ __metadata: languageName: node linkType: hard -"@hs/core@workspace:*, @hs/core@workspace:homeserver/packages/core": - version: 0.0.0-use.local - resolution: "@hs/core@workspace:homeserver/packages/core" - dependencies: - "@hs/crypto": "workspace:*" - "@hs/room": "workspace:*" - bun-types: "npm:latest" - ts-node: "npm:^10.9.2" - ts-patch: "npm:^3.1.2" - typescript: "npm:~5.9.2" - languageName: unknown - linkType: soft - -"@hs/crypto@workspace:*, @hs/crypto@workspace:^, @hs/crypto@workspace:homeserver/packages/crypto": - version: 0.0.0-use.local - resolution: "@hs/crypto@workspace:homeserver/packages/crypto" - dependencies: - "@noble/ed25519": "npm:^3.0.0" - bun-types: "npm:latest" - languageName: unknown - linkType: soft - -"@hs/federation-sdk@workspace:*, @hs/federation-sdk@workspace:^, @hs/federation-sdk@workspace:homeserver/packages/federation-sdk": - version: 0.0.0-use.local - resolution: "@hs/federation-sdk@workspace:homeserver/packages/federation-sdk" - dependencies: - "@hs/core": "workspace:*" - "@hs/room": "workspace:*" - "@rocket.chat/emitter": "npm:^0.31.25" - mongodb: "npm:^6.16.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 - languageName: unknown - linkType: soft - -"@hs/homeserver@workspace:homeserver/packages/homeserver": - version: 0.0.0-use.local - resolution: "@hs/homeserver@workspace:homeserver/packages/homeserver" - dependencies: - "@bogeychan/elysia-etag": "npm:^0.0.6" - "@bogeychan/elysia-logger": "npm:^0.1.4" - "@elysiajs/swagger": "npm:^1.3.0" - "@hs/core": "workspace:*" - "@hs/federation-sdk": "workspace:*" - "@hs/room": "workspace:*" - "@rocket.chat/emitter": "npm:^0.31.25" - bun-types: "npm:latest" - elysia: "npm:^1.1.26" - mongodb: "npm:^6.16.0" - tsyringe: "npm:^4.10.0" - languageName: unknown - linkType: soft - -"@hs/room@workspace:*, @hs/room@workspace:homeserver/packages/room": - version: 0.0.0-use.local - resolution: "@hs/room@workspace:homeserver/packages/room" - dependencies: - "@datastructures-js/priority-queue": "npm:^6.3.3" - "@hs/crypto": "workspace:*" - bun-types: "npm:latest" - zod: "npm:^3.22.4" - languageName: unknown - linkType: soft - "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -4585,13 +4376,6 @@ __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" @@ -7732,12 +7516,11 @@ __metadata: "@babel/core": "npm:~7.26.0" "@babel/preset-env": "npm:~7.26.0" "@babel/preset-typescript": "npm:~7.26.0" - "@hs/crypto": "workspace:^" - "@hs/federation-sdk": "workspace:^" "@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.4" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7754,22 +7537,41 @@ __metadata: marked: "npm:^16.1.2" mongodb: "npm:6.10.0" pino: "npm:8.21.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.4": + version: 0.1.4 + resolution: "@rocket.chat/federation-sdk@npm:0.1.4" + dependencies: + "@rocket.chat/emitter": "npm:^0.31.25" + mongodb: "npm:^6.16.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/9b3a85420d94e2d0314fab5140379c76b0867899cfe004a2ff26ba7609ccecfe8c0e7a45e220d79a7a35998ebfc074d634d6f60f65a36e238db699ce6db42241 + 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" - "@hs/federation-sdk": "workspace:*" "@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.4" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" @@ -7780,9 +7582,11 @@ __metadata: eslint: "npm:~8.45.0" hono: "npm:^3.11.0" pino: "npm:^8.16.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.0" languageName: unknown @@ -8783,6 +8587,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" @@ -10111,54 +9917,6 @@ __metadata: languageName: node linkType: hard -"@scalar/openapi-types@npm:0.1.1": - version: 0.1.1 - resolution: "@scalar/openapi-types@npm:0.1.1" - checksum: 10/d9ad8d3c8846c0ce1525dc945c4e147989a6234cd4f7d00bb15420f82da2f7f6f38f112193bc5e62e4f9791f62704f04b0fdcb2a905c133ec2c86fa791eefdfb - languageName: node - linkType: hard - -"@scalar/openapi-types@npm:0.2.0": - version: 0.2.0 - resolution: "@scalar/openapi-types@npm:0.2.0" - dependencies: - zod: "npm:^3.23.8" - checksum: 10/ee42c8125ebe8ca61bd369124eff2006f83f7b0572413596cfd0eb65aa0c07abb43b6bfbcdaa8fac29139d391fefdff0e029798b352e00e687e07dd69259e79f - languageName: node - linkType: hard - -"@scalar/themes@npm:^0.9.52": - version: 0.9.86 - resolution: "@scalar/themes@npm:0.9.86" - dependencies: - "@scalar/types": "npm:0.1.7" - checksum: 10/683f108624b608358c9f8aefb18a679e059c84f2a98d2ff71ad66a1131fa8ecedc9d2f071ca0d065b7c9b29c350631414e1a1c82b5cf2a5bc06e72849caffd6f - languageName: node - linkType: hard - -"@scalar/types@npm:0.1.7": - version: 0.1.7 - resolution: "@scalar/types@npm:0.1.7" - dependencies: - "@scalar/openapi-types": "npm:0.2.0" - "@unhead/schema": "npm:^1.11.11" - nanoid: "npm:^5.1.5" - type-fest: "npm:^4.20.0" - zod: "npm:^3.23.8" - checksum: 10/cf9a117d960dbfba6187c2e44a73eafb4368d9be85ab6ebe33ebaeda57f10bdac65b53a946afa3de97e8e18b5190a7ea08827ed32709701ac719116df509e661 - languageName: node - linkType: hard - -"@scalar/types@npm:^0.0.12": - version: 0.0.12 - resolution: "@scalar/types@npm:0.0.12" - dependencies: - "@scalar/openapi-types": "npm:0.1.1" - "@unhead/schema": "npm:^1.9.5" - checksum: 10/596fe35b9e8b1823cf72aa7ffbd4998200e09136fc53e4b00b573c9a2c7fa6bf8905e00d5b344337eeef0db02eae0a69e0b6a34ceefdc355ccbc764de9f4b274 - languageName: node - linkType: hard - "@scarf/scarf@npm:=1.4.0": version: 1.4.0 resolution: "@scarf/scarf@npm:1.4.0" @@ -10287,13 +10045,6 @@ __metadata: languageName: node linkType: hard -"@sinclair/typebox@npm:^0.34.33": - version: 0.34.37 - resolution: "@sinclair/typebox@npm:0.34.37" - checksum: 10/bd2ba20a9f7446a353719bc0e6dfab75a13e47af6470fb792e418c585a4eb3bae4f806f87e4067efe2fb0c7686de11e6cf11823a1fe13660892e51cefcfceaea - languageName: node - linkType: hard - "@sindresorhus/is@npm:^0.7.0": version: 0.7.0 resolution: "@sindresorhus/is@npm:0.7.0" @@ -12068,18 +11819,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^5.0.0": - version: 5.0.6 - resolution: "@types/express-serve-static-core@npm:5.0.6" - dependencies: - "@types/node": "npm:*" - "@types/qs": "npm:*" - "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10/9dc51bdee7da9ad4792e97dd1be5b3071b5128f26d3b87a753070221bb36c8f9d16074b95a8b972acc965641e987b1e279a44675e7312ac8f3e18ec9abe93940 - languageName: node - linkType: hard - "@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" @@ -12104,17 +11843,6 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^5.0.1": - version: 5.0.3 - resolution: "@types/express@npm:5.0.3" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^5.0.0" - "@types/serve-static": "npm:*" - checksum: 10/bb6f10c14c8e3cce07f79ee172688aa9592852abd7577b663cd0c2054307f172c2b2b36468c918fed0d4ac359b99695807b384b3da6157dfa79acbac2226b59b - languageName: node - linkType: hard - "@types/fibers@npm:^3.1.4": version: 3.1.4 resolution: "@types/fibers@npm:3.1.4" @@ -12632,15 +12360,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22.15.18": - version: 22.15.33 - resolution: "@types/node@npm:22.15.33" - dependencies: - undici-types: "npm:~6.21.0" - checksum: 10/5734cbca7fc363f3d6ad191e1be645cc9885d642e9f90688892459f10629cf663d2206e7ed7b255dd476baaa86fb011aa09647e77520958b5993b391f793856f - languageName: node - linkType: hard - "@types/node@npm:~22.14.0": version: 22.14.1 resolution: "@types/node@npm:22.14.1" @@ -12946,15 +12665,6 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:^17.0.4": - version: 17.0.4 - resolution: "@types/sinon@npm:17.0.4" - dependencies: - "@types/sinonjs__fake-timers": "npm:*" - checksum: 10/286c34e66e3573673ba59a332ac81189e20dd591c5c5360c8ff3ed83a59a60bdb1d4c8f13ab8863a4d5ce636282e4b11c640b87f398663eee152988ca09b1933 - languageName: node - linkType: hard - "@types/sinonjs__fake-timers@npm:*": version: 8.1.2 resolution: "@types/sinonjs__fake-timers@npm:8.1.2" @@ -13570,16 +13280,6 @@ __metadata: languageName: node linkType: hard -"@unhead/schema@npm:^1.11.11, @unhead/schema@npm:^1.9.5": - version: 1.11.20 - resolution: "@unhead/schema@npm:1.11.20" - dependencies: - hookable: "npm:^5.5.3" - zhead: "npm:^2.2.4" - checksum: 10/150b35c25368c476a2fce030d7b17ff52a3694c7863c288e1ad17ddf1aa0a97314c0732a381b477f9805772d3edb7b492238cd3f1201514b760f521447b239e1 - languageName: node - linkType: hard - "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0": version: 1.9.0 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0" @@ -14416,15 +14116,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^7.0.0": - version: 7.0.0 - resolution: "ansi-escapes@npm:7.0.0" - dependencies: - environment: "npm:^1.0.0" - checksum: 10/2d0e2345087bd7ae6bf122b9cc05ee35560d40dcc061146edcdc02bc2d7c7c50143cd12a22e69a0b5c0f62b948b7bc9a4539ee888b80f5bd33cdfd82d01a70ab - languageName: node - linkType: hard - "ansi-html-community@npm:0.0.8, ansi-html-community@npm:^0.0.8": version: 0.0.8 resolution: "ansi-html-community@npm:0.0.8" @@ -14487,7 +14178,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": +"ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 @@ -16145,15 +15836,6 @@ __metadata: languageName: node linkType: hard -"bun-types@npm:latest": - version: 1.2.17 - resolution: "bun-types@npm:1.2.17" - dependencies: - "@types/node": "npm:*" - checksum: 10/20f8a1fe7cb1375db22947da6ed68178058ac5895259e8ebb89b934540d523621786e831825931cf0aec87239674d839d2c108dab0ca7f258acd7a9539be5aff - languageName: node - linkType: hard - "bundle-name@npm:^4.1.0": version: 4.1.0 resolution: "bundle-name@npm:4.1.0" @@ -16520,7 +16202,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:*, chalk@npm:^5.2.0, chalk@npm:^5.4.1": +"chalk@npm:*, chalk@npm:^5.2.0": version: 5.4.1 resolution: "chalk@npm:5.4.1" checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 @@ -16869,15 +16551,6 @@ __metadata: languageName: node linkType: hard -"cli-cursor@npm:^5.0.0": - version: 5.0.0 - resolution: "cli-cursor@npm:5.0.0" - dependencies: - restore-cursor: "npm:^5.0.0" - checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 - languageName: node - linkType: hard - "cli-spinners@npm:^2.5.0": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -16885,16 +16558,6 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-truncate@npm:4.0.0" - dependencies: - slice-ansi: "npm:^5.0.0" - string-width: "npm:^7.0.0" - checksum: 10/d5149175fd25ca985731bdeec46a55ec237475cf74c1a5e103baea696aceb45e372ac4acbaabf1316f06bd62e348123060f8191ffadfeedebd2a70a2a7fb199d - languageName: node - linkType: hard - "cli-width@npm:^3.0.0": version: 3.0.0 resolution: "cli-width@npm:3.0.0" @@ -17163,13 +16826,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^14.0.0": - version: 14.0.0 - resolution: "commander@npm:14.0.0" - checksum: 10/c05418bfc35a3e8b5c67bd9f75f5b773f386f9b85f83e70e7c926047f270929cb06cf13cd68f387dd6e7e23c6157de8171b28ba606abd3e6256028f1f789becf - languageName: node - linkType: hard - "commander@npm:^2.20.0, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -17440,13 +17096,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^1.0.2": - version: 1.0.2 - resolution: "cookie@npm:1.0.2" - checksum: 10/f5817cdc84d8977761b12549eba29435e675e65c7fef172bc31737788cd8adc83796bf8abe6d950554e7987325ad2d9ac2971c5bd8ff0c4f81c145f82e4ab1be - languageName: node - linkType: hard - "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -19180,13 +18829,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.5.0": - version: 16.5.0 - resolution: "dotenv@npm:16.5.0" - checksum: 10/e68a16834f1a41cc2dfb01563bc150668ad675e6cd09191211467b5c0806b6ecd6ec438e021aa8e01cd0e72d2b70ef4302bec7cc0fe15b6955f85230b62dc8a9 - languageName: node - linkType: hard - "download@npm:^6.2.2": version: 6.2.5 resolution: "download@npm:6.2.5" @@ -19395,30 +19037,6 @@ __metadata: languageName: node linkType: hard -"elysia@npm:^1.1.26": - version: 1.3.5 - resolution: "elysia@npm:1.3.5" - dependencies: - "@sinclair/typebox": "npm:^0.34.33" - cookie: "npm:^1.0.2" - exact-mirror: "npm:0.1.2" - fast-decode-uri-component: "npm:^1.0.1" - openapi-types: "npm:^12.1.3" - peerDependencies: - "@sinclair/typebox": ">= 0.34.0" - exact-mirror: ">= 0.0.9" - file-type: ">= 20.0.0" - openapi-types: ">= 12.0.0" - typescript: ">= 5.0.0" - dependenciesMeta: - "@sinclair/typebox": - optional: true - openapi-types: - optional: true - checksum: 10/162bd82651bd42f52b2530e007ec921887f6b2f957bb9ab1418d59d0590a573ecc16ea2f8333558da8c8d50dafc4c73dfcf5b4113e02002ea2d42e856ebba415 - languageName: node - linkType: hard - "email-validator@npm:^2.0.4": version: 2.0.4 resolution: "email-validator@npm:2.0.4" @@ -19466,13 +19084,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^10.3.0": - version: 10.5.0 - resolution: "emoji-regex@npm:10.5.0" - checksum: 10/97537a2cec7c12653bdedf9d87b3c4e2641f12f8f8829765d33959d8e62c6fc23ffe7722ccbdaf3531681725bed0cc201059652f3289fd06925255437a589a49 - languageName: node - linkType: hard - "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -19650,13 +19261,6 @@ __metadata: languageName: node linkType: hard -"environment@npm:^1.0.0": - version: 1.1.0 - resolution: "environment@npm:1.1.0" - checksum: 10/dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6 - languageName: node - linkType: hard - "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" @@ -20573,18 +20177,6 @@ __metadata: languageName: node linkType: hard -"exact-mirror@npm:0.1.2": - version: 0.1.2 - resolution: "exact-mirror@npm:0.1.2" - peerDependencies: - "@sinclair/typebox": ^0.34.15 - peerDependenciesMeta: - "@sinclair/typebox": - optional: true - checksum: 10/424f5b04605207198d38ba1ef15a577ec6688ebde19afac35ba90f2e01283a31fa30eb4b03ebcafb1c863530d63ef644da092a588ef158503e706fa654078b69 - languageName: node - linkType: hard - "exec-buffer@npm:^3.0.0, exec-buffer@npm:^3.2.0": version: 3.2.0 resolution: "exec-buffer@npm:3.2.0" @@ -20889,20 +20481,6 @@ __metadata: languageName: node linkType: hard -"fast-copy@npm:^3.0.2": - version: 3.0.2 - resolution: "fast-copy@npm:3.0.2" - checksum: 10/97e1022e2aaa27acf4a986d679310bfd66bfb87fe8da9dd33b698e3e50189484001cf1eeb9670e19b59d9d299828ed86c8da354c954f125995ab2a6331c5f290 - languageName: node - linkType: hard - -"fast-decode-uri-component@npm:^1.0.1": - version: 1.0.1 - resolution: "fast-decode-uri-component@npm:1.0.1" - checksum: 10/4b6ed26974414f688be4a15eab6afa997bad4a7c8605cb1deb928b28514817b4523a1af0fa06621c6cbfedb7e5615144c2c3e7512860e3a333a31a28d537dca7 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -21884,13 +21462,6 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0": - version: 1.3.0 - resolution: "get-east-asian-width@npm:1.3.0" - checksum: 10/8e8e779eb28701db7fdb1c8cab879e39e6ae23f52dadd89c8aed05869671cee611a65d4f8557b83e981428623247d8bc5d0c7a4ef3ea7a41d826e73600112ad8 - languageName: node - linkType: hard - "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -22622,13 +22193,6 @@ __metadata: languageName: node linkType: hard -"help-me@npm:^5.0.0": - version: 5.0.0 - resolution: "help-me@npm:5.0.0" - checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 - languageName: node - linkType: hard - "hepburn@npm:^1.2.0": version: 1.2.0 resolution: "hepburn@npm:1.2.0" @@ -22697,30 +22261,6 @@ __metadata: languageName: node linkType: hard -"homeserver@workspace:homeserver": - version: 0.0.0-use.local - resolution: "homeserver@workspace:homeserver" - dependencies: - "@biomejs/biome": "npm:^1.9.4" - "@types/bun": "npm:latest" - "@types/express": "npm:^5.0.1" - "@types/node": "npm:^22.15.18" - "@types/sinon": "npm:^17.0.4" - dotenv: "npm:^16.5.0" - husky: "npm:^9.1.7" - lint-staged: "npm:^16.1.2" - pino: "npm:^9.7.0" - pino-pretty: "npm:^13.0.0" - reflect-metadata: "npm:^0.2.2" - sinon: "npm:^20.0.0" - tsconfig-paths: "npm:^4.2.0" - tsyringe: "npm:^4.10.0" - turbo: "npm:~2.5.6" - tweetnacl: "npm:^1.0.3" - typescript: "npm:~5.9.2" - languageName: unknown - linkType: soft - "hono@npm:^3.11.0": version: 3.12.12 resolution: "hono@npm:3.12.12" @@ -22735,13 +22275,6 @@ __metadata: languageName: node linkType: hard -"hookable@npm:^5.5.3": - version: 5.5.3 - resolution: "hookable@npm:5.5.3" - checksum: 10/c6cec06f693e99a8f8ebd55592efc68042b472a4a04522dde384620d9a2cd7f422003357bf5688525f4bb14454bb0e4188a26db847fb1f1e06875958dfc61cde - languageName: node - linkType: hard - "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -23130,15 +22663,6 @@ __metadata: languageName: node linkType: hard -"husky@npm:^9.1.7": - version: 9.1.7 - resolution: "husky@npm:9.1.7" - bin: - husky: bin.js - checksum: 10/c2412753f15695db369634ba70f50f5c0b7e5cb13b673d0826c411ec1bd9ddef08c1dad89ea154f57da2521d2605bd64308af748749b27d08c5f563bcd89975f - languageName: node - linkType: hard - "hyperdyperid@npm:^1.2.0": version: 1.2.0 resolution: "hyperdyperid@npm:1.2.0" @@ -23930,22 +23454,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^4.0.0": - version: 4.0.0 - resolution: "is-fullwidth-code-point@npm:4.0.0" - checksum: 10/8ae89bf5057bdf4f57b346fb6c55e9c3dd2549983d54191d722d5c739397a903012cc41a04ee3403fd872e811243ef91a7c5196da7b5841dc6b6aae31a264a8d - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^5.0.0": - version: 5.0.0 - resolution: "is-fullwidth-code-point@npm:5.0.0" - dependencies: - get-east-asian-width: "npm:^1.0.0" - checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 - languageName: node - linkType: hard - "is-generator-fn@npm:^2.0.0, is-generator-fn@npm:^2.1.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -26445,40 +25953,6 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:^16.1.2": - version: 16.1.2 - resolution: "lint-staged@npm:16.1.2" - dependencies: - chalk: "npm:^5.4.1" - commander: "npm:^14.0.0" - debug: "npm:^4.4.1" - lilconfig: "npm:^3.1.3" - listr2: "npm:^8.3.3" - micromatch: "npm:^4.0.8" - nano-spawn: "npm:^1.0.2" - pidtree: "npm:^0.6.0" - string-argv: "npm:^0.3.2" - yaml: "npm:^2.8.0" - bin: - lint-staged: bin/lint-staged.js - checksum: 10/90df77c2f59cdc5ebeb8a60767f07025a8aed9161f604fea6cf1ca895ff3b56995a00145a3e0b5c0bf22e8f667a6182256b68e001e5f3118e46a3c5150bede82 - languageName: node - linkType: hard - -"listr2@npm:^8.3.3": - version: 8.3.3 - resolution: "listr2@npm:8.3.3" - dependencies: - cli-truncate: "npm:^4.0.0" - colorette: "npm:^2.0.20" - eventemitter3: "npm:^5.0.1" - log-update: "npm:^6.1.0" - rfdc: "npm:^1.4.1" - wrap-ansi: "npm:^9.0.0" - checksum: 10/92f1bb60e9a0f4fed9bff89fbab49d80fc889d29cf47c0a612f5a62a036dead49d3f697d3a79e36984768529bd3bfacb3343859eafceba179a8e66c034d99300 - languageName: node - linkType: hard - "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -26719,19 +26193,6 @@ __metadata: languageName: node linkType: hard -"log-update@npm:^6.1.0": - version: 6.1.0 - resolution: "log-update@npm:6.1.0" - dependencies: - ansi-escapes: "npm:^7.0.0" - cli-cursor: "npm:^5.0.0" - slice-ansi: "npm:^7.1.0" - strip-ansi: "npm:^7.1.0" - wrap-ansi: "npm:^9.0.0" - checksum: 10/5abb4131e33b1e7f8416bb194fe17a3603d83e4657c5bf5bb81ce4187f3b00ea481643b85c3d5cefe6037a452cdcf7f1391ab8ea0d9c23e75d19589830ec4f11 - languageName: node - linkType: hard - "logform@npm:^2.6.0, logform@npm:^2.6.1": version: 2.6.1 resolution: "logform@npm:2.6.1" @@ -27558,13 +27019,6 @@ __metadata: languageName: node linkType: hard -"mimic-function@npm:^5.0.0": - version: 5.0.1 - resolution: "mimic-function@npm:5.0.1" - checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c - languageName: node - linkType: hard - "mimic-response@npm:^1.0.0": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -28198,13 +27652,6 @@ __metadata: languageName: node linkType: hard -"nano-spawn@npm:^1.0.2": - version: 1.0.2 - resolution: "nano-spawn@npm:1.0.2" - checksum: 10/6ce9e60846d2e37c0e3cd048472683c81dbcaadef9ebe73bfc8754ee7da2a574f724436d3dcdeda5d807aedc857cc8cbc278a9882529164b5ef4b170b95cfe0b - languageName: node - linkType: hard - "nanoid@npm:3.3.1": version: 3.3.1 resolution: "nanoid@npm:3.3.1" @@ -28232,15 +27679,6 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^5.1.5": - version: 5.1.5 - resolution: "nanoid@npm:5.1.5" - bin: - nanoid: bin/nanoid.js - checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 - languageName: node - linkType: hard - "napi-build-utils@npm:^2.0.0": version: 2.0.0 resolution: "napi-build-utils@npm:2.0.0" @@ -28996,15 +28434,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^7.0.0": - version: 7.0.0 - resolution: "onetime@npm:7.0.0" - dependencies: - mimic-function: "npm:^5.0.0" - checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c - languageName: node - linkType: hard - "only@npm:^0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -29013,14 +28442,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 @@ -29035,13 +28464,6 @@ __metadata: languageName: node linkType: hard -"openapi-types@npm:^12.1.3": - version: 12.1.3 - resolution: "openapi-types@npm:12.1.3" - checksum: 10/9d1d7ed848622b63d0a4c3f881689161b99427133054e46b8e3241e137f1c78bb0031c5d80b420ee79ac2e91d2e727ffd6fc13c553d1b0488ddc8ad389dcbef8 - languageName: node - linkType: hard - "opentracing@npm:^0.14.4": version: 0.14.7 resolution: "opentracing@npm:0.14.7" @@ -29807,13 +29229,6 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.2": - version: 1.1.2 - resolution: "pathe@npm:1.1.2" - checksum: 10/f201d796351bf7433d147b92c20eb154a4e0ea83512017bf4ec4e492a5d6e738fb45798be4259a61aa81270179fce11026f6ff0d3fa04173041de044defe9d80 - languageName: node - linkType: hard - "pathington@npm:^1.1.7": version: 1.1.7 resolution: "pathington@npm:1.1.7" @@ -29942,15 +29357,6 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:^0.6.0": - version: 0.6.0 - resolution: "pidtree@npm:0.6.0" - bin: - pidtree: bin/pidtree.js - checksum: 10/ea67fb3159e170fd069020e0108ba7712df9f0fd13c8db9b2286762856ddce414fb33932e08df4bfe36e91fe860b51852aee49a6f56eb4714b69634343add5df - languageName: node - linkType: hard - "pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -30008,38 +29414,6 @@ __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:^13.0.0": - version: 13.0.0 - resolution: "pino-pretty@npm:13.0.0" - dependencies: - colorette: "npm:^2.0.7" - dateformat: "npm:^4.6.3" - fast-copy: "npm:^3.0.2" - fast-safe-stringify: "npm:^2.1.1" - help-me: "npm:^5.0.0" - joycon: "npm:^3.1.1" - minimist: "npm:^1.2.6" - on-exit-leak-free: "npm:^2.1.0" - pino-abstract-transport: "npm:^2.0.0" - pump: "npm:^3.0.0" - secure-json-parse: "npm:^2.4.0" - sonic-boom: "npm:^4.0.1" - strip-json-comments: "npm:^3.1.1" - bin: - pino-pretty: bin.js - checksum: 10/9861fdbe88db000e3b0fe959f0fb7b5913e8d16af70373155d48854c5d509629e7e1ba09ed3fac24a9bd2729451567a698938b9741d84de63eb549843450e71c - languageName: node - linkType: hard - "pino-pretty@npm:^7.6.1": version: 7.6.1 resolution: "pino-pretty@npm:7.6.1" @@ -30070,13 +29444,6 @@ __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, pino@npm:^8.16.0, pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" @@ -30098,27 +29465,6 @@ __metadata: languageName: node linkType: hard -"pino@npm:^9.6.0, pino@npm:^9.7.0": - version: 9.10.0 - resolution: "pino@npm:9.10.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/02962e12ae7692c763da6f64c2037f113a03f5f7e6e6be96f0878c4aa4095ca8cf8ffbac561eda1ac367c7ea1f7ee50d44d211674f314b2560ddd3db1e4efb5f - 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" @@ -31167,13 +30513,6 @@ __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" @@ -32840,16 +32179,6 @@ __metadata: languageName: node linkType: hard -"restore-cursor@npm:^5.0.0": - version: 5.1.0 - resolution: "restore-cursor@npm:5.1.0" - dependencies: - onetime: "npm:^7.0.0" - signal-exit: "npm:^4.1.0" - checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c - languageName: node - linkType: hard - "restructure@npm:^3.0.0": version: 3.0.0 resolution: "restructure@npm:3.0.0" @@ -32903,13 +32232,6 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.4.1": - version: 1.4.1 - resolution: "rfdc@npm:1.4.1" - checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729 - languageName: node - linkType: hard - "rimraf@npm:^2.5.4": version: 2.7.1 resolution: "rimraf@npm:2.7.1" @@ -33848,7 +33170,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": +"signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -33896,19 +33218,6 @@ __metadata: languageName: node linkType: hard -"sinon@npm:^20.0.0": - version: 20.0.0 - resolution: "sinon@npm:20.0.0" - dependencies: - "@sinonjs/commons": "npm:^3.0.1" - "@sinonjs/fake-timers": "npm:^13.0.5" - "@sinonjs/samsam": "npm:^8.0.1" - diff: "npm:^7.0.0" - supports-color: "npm:^7.2.0" - checksum: 10/825cb36a58c0510cec03d9bef4fe66a12baf0e0cfdf1600423e3da1e6d57a03fe8161f4859340ea13d4c42e63da1724a260ef4c5ce119dc9ee075ad93b6e8bdd - languageName: node - linkType: hard - "sip-methods@npm:^0.3.0": version: 0.3.0 resolution: "sip-methods@npm:0.3.0" @@ -33976,26 +33285,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^5.0.0": - version: 5.0.0 - resolution: "slice-ansi@npm:5.0.0" - dependencies: - ansi-styles: "npm:^6.0.0" - is-fullwidth-code-point: "npm:^4.0.0" - checksum: 10/7e600a2a55e333a21ef5214b987c8358fe28bfb03c2867ff2cbf919d62143d1812ac27b4297a077fdaf27a03da3678e49551c93e35f9498a3d90221908a1180e - languageName: node - linkType: hard - -"slice-ansi@npm:^7.1.0": - version: 7.1.0 - resolution: "slice-ansi@npm:7.1.0" - dependencies: - ansi-styles: "npm:^6.2.1" - is-fullwidth-code-point: "npm:^5.0.0" - checksum: 10/10313dd3cf7a2e4b265f527b1684c7c568210b09743fd1bd74f2194715ed13ffba653dc93a5fa79e3b1711518b8990a732cb7143aa01ddafe626e99dfa6474b2 - languageName: node - linkType: hard - "slick@npm:^1.12.2": version: 1.12.2 resolution: "slick@npm:1.12.2" @@ -34094,15 +33383,6 @@ __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" @@ -34653,13 +33933,6 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:^0.3.2": - version: 0.3.2 - resolution: "string-argv@npm:0.3.2" - checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af - languageName: node - linkType: hard - "string-length@npm:^4.0.1, string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -34716,17 +33989,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^7.0.0": - version: 7.2.0 - resolution: "string-width@npm:7.2.0" - dependencies: - emoji-regex: "npm:^10.3.0" - get-east-asian-width: "npm:^1.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10/42f9e82f61314904a81393f6ef75b832c39f39761797250de68c041d8ba4df2ef80db49ab6cd3a292923a6f0f409b8c9980d120f7d32c820b4a8a84a2598a295 - languageName: node - linkType: hard - "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -34861,7 +34123,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": +"strip-ansi@npm:^7.0.1": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -35634,15 +34896,6 @@ __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" @@ -36105,7 +35358,7 @@ __metadata: languageName: node linkType: hard -"ts-patch@npm:^3.1.2, ts-patch@npm:^3.3.0": +"ts-patch@npm:^3.3.0": version: 3.3.0 resolution: "ts-patch@npm:3.3.0" dependencies: @@ -36399,7 +35652,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.20.0, type-fest@npm:^4.41.0": +"type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 @@ -37954,17 +37207,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^9.0.0": - version: 9.0.0 - resolution: "wrap-ansi@npm:9.0.0" - dependencies: - ansi-styles: "npm:^6.2.1" - string-width: "npm:^7.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10/b9d91564c091cf3978a7c18ca0f3e4d4606e83549dbe59cf76f5e77feefdd5ec91443155e8102630524d10a8c275efac8a7082c0f26fa43e6b989dc150d176ce - languageName: node - linkType: hard - "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -38034,6 +37276,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" @@ -38341,13 +37592,6 @@ __metadata: languageName: node linkType: hard -"zhead@npm:^2.2.4": - version: 2.2.4 - resolution: "zhead@npm:2.2.4" - checksum: 10/cfa2ba81bf936fd4f5ba19360412c7017a164250823f22e575e1956b20c73d76b989985c02a4f89e2e02f3fb203fbe8857072cf5fbece59a374d1a6bf588555b - languageName: node - linkType: hard - "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1" @@ -38359,13 +37603,20 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.0, zod@npm:^3.22.4, zod@npm:^3.23.8": +"zod@npm:^3.22.0": version: 3.25.67 resolution: "zod@npm:3.25.67" checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa 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" From 5e1a275c2a7c298717ba46ffcbd8097dbe59ad5c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 24 Sep 2025 17:48:20 -0300 Subject: [PATCH 83/99] fix(reaction): ensure beforeReacted is awaited in setReaction function --- apps/meteor/app/reactions/server/setReaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 0559ccfefad74..7d30963e82cd6 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; +import { api, Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; @@ -34,7 +34,7 @@ export const removeUserReaction = (message: IMessage, reaction: string, username }; export async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction: string, userAlreadyReacted?: boolean) { - // await Message.beforeReacted(message, room); + await Message.beforeReacted(message, room); if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { From 863721258a8fb7d9d45646a3432993b0f6e53eb7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 24 Sep 2025 18:08:30 -0300 Subject: [PATCH 84/99] chore(federation): increase time to accept DMs (#37054) --- .../src/api/_matrix/invite.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index e86c6ecfa4e36..f61ace906b70b 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -328,14 +328,17 @@ export const getMatrixInviteRoutes = (services: HomeserverServices) => { const inviteEvent = await invite.processInvite(event, roomId, eventId, roomVersion); - setTimeout(() => { - void startJoiningRoom({ - inviteEvent, - user: ourUser, - room, - state, - }); - }, 200); + setTimeout( + () => { + void startJoiningRoom({ + inviteEvent, + user: ourUser, + room, + state, + }); + }, + inviteEvent.event.content.is_direct ? 2000 : 0, + ); return { body: { From ae7b86a651436891d637f3de28338ce96743af59 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 24 Sep 2025 18:24:40 -0300 Subject: [PATCH 85/99] chore: upgrade federation zod --- ee/apps/federation-service/package.json | 2 +- yarn.lock | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 8baefa653817d..518862a0bc43e 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -37,7 +37,7 @@ "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", "tweetnacl": "^1.0.3", - "zod": "^3.22.0" + "zod": "^3.24.1" }, "devDependencies": { "@types/bun": "latest", diff --git a/yarn.lock b/yarn.lock index ee8a8957457e0..90d1054b3cd44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7588,7 +7588,7 @@ __metadata: tsyringe: "npm:^4.10.0" tweetnacl: "npm:^1.0.3" typescript: "npm:^5.3.0" - zod: "npm:^3.22.0" + zod: "npm:^3.24.1" languageName: unknown linkType: soft @@ -37603,13 +37603,6 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.0": - version: 3.25.67 - resolution: "zod@npm:3.25.67" - checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa - languageName: node - linkType: hard - "zod@npm:^3.22.4": version: 3.25.76 resolution: "zod@npm:3.25.76" From 0a0325e8124ddbb44fb9f34575b6c6a7bed11cd3 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 24 Sep 2025 18:43:49 -0300 Subject: [PATCH 86/99] chore: bump federation-sdk --- ee/apps/federation-service/package.json | 2 +- ee/packages/federation-matrix/package.json | 2 +- yarn.lock | 21 +++++++-------------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 518862a0bc43e..44aa8b12d6bdb 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/core-typings": "workspace:*", "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", - "@rocket.chat/federation-sdk": "0.1.4", + "@rocket.chat/federation-sdk": "0.1.5", "@rocket.chat/http-router": "workspace:*", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index a1578d10777ca..9bb407054bf32 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -38,7 +38,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.1.4", + "@rocket.chat/federation-sdk": "0.1.5", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 90d1054b3cd44..20b9085af93de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7520,7 +7520,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.4" + "@rocket.chat/federation-sdk": "npm:0.1.5" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7546,19 +7546,19 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.1.4": - version: 0.1.4 - resolution: "@rocket.chat/federation-sdk@npm:0.1.4" +"@rocket.chat/federation-sdk@npm:0.1.5": + version: 0.1.5 + resolution: "@rocket.chat/federation-sdk@npm:0.1.5" dependencies: "@rocket.chat/emitter": "npm:^0.31.25" mongodb: "npm:^6.16.0" reflect-metadata: "npm:^0.2.2" tsyringe: "npm:^4.10.0" tweetnacl: "npm:^1.0.3" - zod: "npm:^3.22.4" + zod: "npm:^3.24.1" peerDependencies: typescript: ~5.9.2 - checksum: 10/9b3a85420d94e2d0314fab5140379c76b0867899cfe004a2ff26ba7609ccecfe8c0e7a45e220d79a7a35998ebfc074d634d6f60f65a36e238db699ce6db42241 + checksum: 10/da8b11e77ba855873e3465e7172f21923a18a458207eeff62afceddc7730f5a28a43b1133b043a9d8655f87b0b28f4304d53343eca90f45f3b1a65d9d95087c9 languageName: node linkType: hard @@ -7571,7 +7571,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:*" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.4" + "@rocket.chat/federation-sdk": "npm:0.1.5" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" @@ -37603,13 +37603,6 @@ __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" From c45a6d4faaef0c226534e0276debdcf7df7bc53f Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Wed, 24 Sep 2025 19:23:39 -0300 Subject: [PATCH 87/99] fix: create federated user on DMs from RC > remote flow (#37043) Co-authored-by: Guilherme Gazzo --- .../lib/server/functions/createDirectRoom.ts | 6 +-- .../app/lib/server/functions/createRoom.ts | 9 +++- apps/meteor/lib/callbacks.ts | 2 +- .../federation-matrix/src/FederationMatrix.ts | 47 ++++--------------- .../src/types/IFederationMatrixService.ts | 2 +- 5 files changed, 23 insertions(+), 43 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index c1867de9feb03..2e0f34c0e1f31 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -42,7 +42,7 @@ const getName = (members: IUser[]): string => members.map(({ username }) => user export async function createDirectRoom( members: IUser[] | string[], - roomExtraData = {}, + roomExtraData: Partial = {}, options: { creator?: string; subscriptionExtra?: ISubscriptionExtraData; @@ -69,6 +69,8 @@ export async function createDirectRoom( }) .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(); @@ -97,8 +99,6 @@ export async function createDirectRoom( ...roomExtraData, }; - await callbacks.run('beforeCreateDirectRoom', members, roomInfo); - if (isNewRoom) { const tmpRoom: { _USERNAMES?: (string | undefined)[] } & typeof roomInfo = { ...roomInfo, diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 3302425fbc58f..af078945ef0be 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -134,9 +134,16 @@ export const createRoom = async ( > => { 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, - ...(optionalExtraData.federated && { + ...((hasFederatedMembers || optionalExtraData.federated) && { federated: true, federation: { version: 1, diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index e9c004e146ccd..7fe2240f2342e 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -78,7 +78,7 @@ interface EventLikeCallbackSignatures { options?: ICreateRoomOptions; }, ) => void; - 'beforeCreateDirectRoom': (members: IUser[], room: IRoom) => void; + 'beforeCreateDirectRoom': (members: string[], room: IRoom) => void; 'federation.beforeCreateDirectMessage': (members: IUser[]) => void; 'afterSetReaction': (message: IMessage, params: { user: IUser; reaction: string; shouldReact: boolean; room: IRoom }) => void; 'afterUnsetReaction': ( diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 9cebe72368cc4..18e3455c5be37 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -256,47 +256,24 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } - async ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise { + async ensureFederatedUsersExistLocally(usernames: string[]): Promise { try { - this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: members.length }); + this.logger.debug('Ensuring federated users exist locally before DM creation', { memberCount: usernames.length }); - for await (const member of members) { - let username: string; - - if (typeof member === 'string') { - username = member; - } else if (typeof member.username === 'string') { - username = member.username; - } else { - continue; - } - - if (!username.includes(':') && !username.includes('@')) { + 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) { - // TODO review: DM - // const existingBridge = await MatrixBridgedUser.getExternalUserIdByLocalUserId(existingUser._id); // TODO review: DM - // if (!existingBridge) { - // const remoteDomain = externalUserId.split(':')[1] || this.serverName; - // await MatrixBridgedUser.createOrUpdateByLocalId(existingUser._id, externalUserId, true, remoteDomain); - // } continue; } - // TODO: there is not need to check if the username includes ':' or '@', we should just use the username as is - const externalUserId = username.includes(':') ? `@${username}` : `@${username}:${this.serverName}`; - this.logger.debug('Creating federated user locally', { externalUserId, username }); - - const remoteDomain = externalUserId.split(':')[1]; - - const localName = username.split(':')[0]?.replace('@', '') || username; - - const newUser = { + await Users.create({ username, - name: localName, + name: username, type: 'user' as const, status: UserStatus.OFFLINE, active: true, @@ -305,16 +282,12 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS federated: true, federation: { version: 1, - mui: externalUserId, - origin: remoteDomain, + mui: username, + origin: username.split(':')[1], }, createdAt: new Date(), _updatedAt: new Date(), - }; - - const { insertedId } = await Users.insertOne(newUser); - - this.logger.debug('Successfully created federated user locally', { userId: insertedId, externalUserId }); + }); } } catch (error) { this.logger.error('Failed to ensure federated users exist locally:', error); diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 28bf89012b7b5..893bca7be5f3c 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -7,7 +7,7 @@ export interface IFederationMatrixService { wellKnown: Router<'/.well-known'>; }; createRoom(room: IRoomFederated, owner: IUser, members: string[]): Promise<{ room_id: string; event_id: string }>; - ensureFederatedUsersExistLocally(members: (IUser | string)[]): Promise; + 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; From 62ea37571f76d8191599445f8b6327af83515194 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 24 Sep 2025 19:42:39 -0300 Subject: [PATCH 88/99] bump: update federation-sdk to version 0.1.6 --- ee/apps/federation-service/package.json | 2 +- ee/packages/federation-matrix/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 44aa8b12d6bdb..99a70942f1abf 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/core-typings": "workspace:*", "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", - "@rocket.chat/federation-sdk": "0.1.5", + "@rocket.chat/federation-sdk": "0.1.6", "@rocket.chat/http-router": "workspace:*", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 9bb407054bf32..77331c8b7da6a 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -38,7 +38,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.1.5", + "@rocket.chat/federation-sdk": "0.1.6", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 20b9085af93de..cdd69f3de332b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7520,7 +7520,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.5" + "@rocket.chat/federation-sdk": "npm:0.1.6" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7546,9 +7546,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.1.5": - version: 0.1.5 - resolution: "@rocket.chat/federation-sdk@npm:0.1.5" +"@rocket.chat/federation-sdk@npm:0.1.6": + version: 0.1.6 + resolution: "@rocket.chat/federation-sdk@npm:0.1.6" dependencies: "@rocket.chat/emitter": "npm:^0.31.25" mongodb: "npm:^6.16.0" @@ -7558,7 +7558,7 @@ __metadata: zod: "npm:^3.24.1" peerDependencies: typescript: ~5.9.2 - checksum: 10/da8b11e77ba855873e3465e7172f21923a18a458207eeff62afceddc7730f5a28a43b1133b043a9d8655f87b0b28f4304d53343eca90f45f3b1a65d9d95087c9 + checksum: 10/0254e1dcdbf687773a8aa44b206d5c4b0b79fcc329896065120fba387bcb29cdaf77bbe2929cd4b0c7cb6d7d1569911929a56fe1f66df47712f2e2377f358805 languageName: node linkType: hard @@ -7571,7 +7571,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:*" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.5" + "@rocket.chat/federation-sdk": "npm:0.1.6" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" From 712630f9e14ce4f274cf5a792ee443885c3676bc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 24 Sep 2025 22:37:40 -0300 Subject: [PATCH 89/99] chore: add @datastructures-js/heap and @datastructures-js/priority-queue dependencies --- apps/meteor/package.json | 1 + yarn.lock | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index d3229d1877cdf..5913b86ccb49c 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.4", "@google-cloud/storage": "^7.15.0", "@kaciras/deasync": "^1.1.0", "@nivo/bar": "0.88.0", diff --git a/yarn.lock b/yarn.lock index cdd69f3de332b..98a3c99ff7fed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2348,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.4": + 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" @@ -8214,6 +8230,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.4" "@faker-js/faker": "npm:~8.0.2" "@google-cloud/storage": "npm:^7.15.0" "@kaciras/deasync": "npm:^1.1.0" From 3bb316bdcdca245d8a462c8cdfa7d47244b2cbec Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 24 Sep 2025 23:54:47 -0300 Subject: [PATCH 90/99] bump: federation-sdk --- ee/apps/federation-service/package.json | 2 +- ee/packages/federation-matrix/package.json | 2 +- yarn.lock | 95 ++++++++++++++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 99a70942f1abf..b25ddde627a4f 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/core-typings": "workspace:*", "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", - "@rocket.chat/federation-sdk": "0.1.6", + "@rocket.chat/federation-sdk": "0.1.7", "@rocket.chat/http-router": "workspace:*", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 77331c8b7da6a..0585149d7b99b 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -38,7 +38,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.1.6", + "@rocket.chat/federation-sdk": "0.1.7", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 98a3c99ff7fed..cbce184792af3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2355,7 +2355,7 @@ __metadata: languageName: node linkType: hard -"@datastructures-js/priority-queue@npm:^6.3.4": +"@datastructures-js/priority-queue@npm:^6.3.3, @datastructures-js/priority-queue@npm:^6.3.4": version: 6.3.4 resolution: "@datastructures-js/priority-queue@npm:6.3.4" dependencies: @@ -4392,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" @@ -7536,7 +7543,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.6" + "@rocket.chat/federation-sdk": "npm:0.1.7" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7562,19 +7569,22 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.1.6": - version: 0.1.6 - resolution: "@rocket.chat/federation-sdk@npm:0.1.6" +"@rocket.chat/federation-sdk@npm:0.1.7": + version: 0.1.7 + resolution: "@rocket.chat/federation-sdk@npm:0.1.7" 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.24.1" + zod: "npm:^3.22.4" peerDependencies: typescript: ~5.9.2 - checksum: 10/0254e1dcdbf687773a8aa44b206d5c4b0b79fcc329896065120fba387bcb29cdaf77bbe2929cd4b0c7cb6d7d1569911929a56fe1f66df47712f2e2377f358805 + checksum: 10/06ce073279e01b629b4ddac301dbf67fff9521046d6586c9e0799df194a15d1ab69be1e3ea0820804e376cecc0c2855327c3307ae985faec5540047d4dac09e6 languageName: node linkType: hard @@ -7587,7 +7597,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:*" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.6" + "@rocket.chat/federation-sdk": "npm:0.1.7" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" @@ -29431,6 +29441,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" @@ -29461,6 +29480,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, pino@npm:^8.16.0, pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" @@ -29482,6 +29508,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" @@ -30530,6 +30577,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" @@ -33400,6 +33454,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" @@ -34913,6 +34976,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" @@ -37620,6 +37692,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" From a20cc72fd6b3f22671262861ef6088ee8ef1ae59 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 00:10:43 -0300 Subject: [PATCH 91/99] chore: remover emoji converter file --- .../src/utils/emojiConverter.ts | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 ee/packages/federation-matrix/src/utils/emojiConverter.ts diff --git a/ee/packages/federation-matrix/src/utils/emojiConverter.ts b/ee/packages/federation-matrix/src/utils/emojiConverter.ts deleted file mode 100644 index f73483380343e..0000000000000 --- a/ee/packages/federation-matrix/src/utils/emojiConverter.ts +++ /dev/null @@ -1,45 +0,0 @@ -const EMOJI_MAP: Record = { - ':thumbsup:': '👍', - ':thumbsdown:': '👎', - ':heart:': '❤️', - ':smile:': '😊', - ':laughing:': '😂', - ':cry:': '😢', - ':angry:': '😠', - ':star:': '⭐', - ':fire:': '🔥', - ':clap:': '👏', - ':ok_hand:': '👌', - ':wave:': '👋', - ':+1:': '👍', - ':-1:': '👎', - ':100:': '💯', - ':rocket:': '🚀', - ':eyes:': '👀', - ':thinking:': '🤔', - ':party:': '🎉', - ':tada:': '🎉', -}; - -export function convertEmojiToUnicode(reaction: string): string { - if (!reaction.startsWith(':') || !reaction.endsWith(':')) { - return reaction; - } - - const unicode = EMOJI_MAP[reaction]; - if (unicode) { - return unicode; - } - - return reaction.slice(1, -1); -} - -export function convertUnicodeToEmoji(unicode: string): string { - for (const [shortcode, emoji] of Object.entries(EMOJI_MAP)) { - if (emoji === unicode) { - return shortcode; - } - } - - return unicode; -} From 5896129d6c545b72be83017ed18d43bfda4ceb9e Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 00:59:56 -0300 Subject: [PATCH 92/99] add last missing dep --- apps/meteor/package.json | 3 ++- yarn.lock | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5913b86ccb49c..7687c384abfbe 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -225,7 +225,7 @@ "@babel/runtime": "~7.26.10", "@bugsnag/js": "~7.20.2", "@bugsnag/plugin-react": "~7.19.0", - "@datastructures-js/priority-queue": "^6.3.4", + "@datastructures-js/priority-queue": "^6.3.3", "@google-cloud/storage": "^7.15.0", "@kaciras/deasync": "^1.1.0", "@nivo/bar": "0.88.0", @@ -233,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", diff --git a/yarn.lock b/yarn.lock index cbce184792af3..35f44cf2301e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2355,7 +2355,7 @@ __metadata: languageName: node linkType: hard -"@datastructures-js/priority-queue@npm:^6.3.3, @datastructures-js/priority-queue@npm:^6.3.4": +"@datastructures-js/priority-queue@npm:^6.3.3": version: 6.3.4 resolution: "@datastructures-js/priority-queue@npm:6.3.4" dependencies: @@ -8240,7 +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.4" + "@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" @@ -8249,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" From 37c5bf71f2cd963fb6ef59df58f581ba51106177 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 15:04:16 -0300 Subject: [PATCH 93/99] align dep version --- ee/apps/federation-service/package.json | 4 ++-- ee/packages/federation-matrix/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index b25ddde627a4f..6b806cba22833 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -32,12 +32,12 @@ "@rocket.chat/models": "workspace:*", "@rocket.chat/network-broker": "workspace:^", "hono": "^3.11.0", - "pino": "^8.16.0", + "pino": "^9.11.0", "polka": "^0.5.2", "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", "tweetnacl": "^1.0.3", - "zod": "^3.24.1" + "zod": "^3.22.4" }, "devDependencies": { "@types/bun": "latest", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 0585149d7b99b..5998f5994b39b 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -48,7 +48,7 @@ "emojione": "^4.5.0", "marked": "^16.1.2", "mongodb": "6.10.0", - "pino": "8.21.0", + "pino": "^9.11.0", "reflect-metadata": "^0.2.2", "sanitize-html": "^2.17.0", "tsyringe": "^4.10.0", diff --git a/yarn.lock b/yarn.lock index 35f44cf2301e6..2fa9bd7b25b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7559,7 +7559,7 @@ __metadata: jest: "npm:~30.0.0" marked: "npm:^16.1.2" mongodb: "npm:6.10.0" - pino: "npm:8.21.0" + pino: "npm:^9.11.0" pino-pretty: "npm:^7.6.1" reflect-metadata: "npm:^0.2.2" sanitize-html: "npm:^2.17.0" @@ -7607,14 +7607,14 @@ __metadata: "@types/express": "npm:^4.17.17" eslint: "npm:~8.45.0" hono: "npm:^3.11.0" - pino: "npm:^8.16.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.24.1" + zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -29488,7 +29488,7 @@ __metadata: languageName: node linkType: hard -"pino@npm:8.21.0, pino@npm:^8.16.0, pino@npm:^8.21.0": +"pino@npm:^8.21.0": version: 8.21.0 resolution: "pino@npm:8.21.0" dependencies: From 1a32eb41ed520a7e634b0239244aaad9d3101afe Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Sep 2025 17:56:05 -0300 Subject: [PATCH 94/99] fix(federation): uploads not working properly (#37064) --- .../federation-matrix/src/FederationMatrix.ts | 2 +- .../federation-matrix/src/events/message.ts | 15 +++++++++++++-- .../src/services/MatrixMediaService.ts | 5 ++++- packages/core-typings/src/IUpload.ts | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 18e3455c5be37..6e7e687da847e 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -428,7 +428,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS let lastEventId: { eventId: string } | null = null; for await (const file of message.files) { - const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain); + const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain, matrixRoomId); const msgtype = this.getMatrixMessageType(file.type); const fileContent = { diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 84f266a5a16f7..93dd91856d766 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -29,6 +29,7 @@ async function handleMediaMessage( messageBody: string, user: IUser, room: IRoom, + matrixRoomId: string, eventId: EventID, tmid?: string, ): Promise<{ @@ -42,7 +43,7 @@ async function handleMediaMessage( const mimeType = fileInfo?.mimetype; const fileName = messageBody; - const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(url, { + const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(url, matrixRoomId, { name: messageBody, size: fileInfo?.size, type: mimeType, @@ -226,7 +227,17 @@ export function message(emitter: Emitter, serverName: 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.event_id, thread?.tmid); + 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({ diff --git a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts index 5e4761b1d35df..e19273f9e9a50 100644 --- a/ee/packages/federation-matrix/src/services/MatrixMediaService.ts +++ b/ee/packages/federation-matrix/src/services/MatrixMediaService.ts @@ -38,7 +38,7 @@ export class MatrixMediaService { }; } - static async prepareLocalFileForMatrix(fileId: string, serverName: string): Promise { + static async prepareLocalFileForMatrix(fileId: string, serverName: string, matrixRoomId: string): Promise { try { const file = await Uploads.findOneById(fileId); if (!file) { @@ -53,6 +53,7 @@ export class MatrixMediaService { const mxcUri = this.generateMXCUri(fileId, serverName); await Uploads.setFederationInfo(fileId, { + mrid: matrixRoomId, mxcUri, serverName, mediaId: fileId, @@ -86,6 +87,7 @@ export class MatrixMediaService { static async downloadAndStoreRemoteFile( mxcUri: string, + matrixRoomId: string, metadata: { name: string; size?: number; @@ -131,6 +133,7 @@ export class MatrixMediaService { await Uploads.setFederationInfo(uploadedFile._id, { mxcUri, + mrid: matrixRoomId, serverName: parts.serverName, mediaId: parts.mediaId, }); diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 2eb5cf0741a73..e487151b94bf5 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -61,6 +61,7 @@ export interface IUpload { }; federation?: { mxcUri: string; + mrid: string; serverName: string; mediaId: string; }; From 41768c5d54bed0b7dffd6ad00e9dff05df84c64d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Sep 2025 18:58:27 -0300 Subject: [PATCH 95/99] feat(federation): accept remote join at 3rd party (#37072) Co-authored-by: Diego Sampaio --- .../federation-matrix/src/events/index.ts | 2 - .../federation-matrix/src/events/invite.ts | 52 -------- .../federation-matrix/src/events/member.ts | 113 +++++++++++++----- 3 files changed, 83 insertions(+), 84 deletions(-) delete mode 100644 ee/packages/federation-matrix/src/events/invite.ts diff --git a/ee/packages/federation-matrix/src/events/index.ts b/ee/packages/federation-matrix/src/events/index.ts index a8b3fb27718d1..9cf3abd0a9539 100644 --- a/ee/packages/federation-matrix/src/events/index.ts +++ b/ee/packages/federation-matrix/src/events/index.ts @@ -2,7 +2,6 @@ import type { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk'; import { edus } from './edu'; -import { invite } from './invite'; import { member } from './member'; import { message } from './message'; import { ping } from './ping'; @@ -16,7 +15,6 @@ export function registerEvents( ) { ping(emitter); message(emitter, serverName); - invite(emitter); reaction(emitter); member(emitter); edus(emitter, eduProcessTypes); diff --git a/ee/packages/federation-matrix/src/events/invite.ts b/ee/packages/federation-matrix/src/events/invite.ts deleted file mode 100644 index 9d0197e8731b4..0000000000000 --- a/ee/packages/federation-matrix/src/events/invite.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 { Rooms, Users } from '@rocket.chat/models'; - -export function invite(emitter: Emitter) { - emitter.on('homeserver.matrix.accept-invite', async (data) => { - const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); - if (!room) { - console.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.ONLINE, - 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); - }); -} diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index 672afe91149f5..89787f46a3a61 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -1,4 +1,5 @@ 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'; @@ -6,44 +7,96 @@ 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 { - // Only handle leave events (including kicks) - if (data.content.membership !== 'leave') { - logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); - return; + if (data.content.membership === 'leave') { + return membershipLeaveAction(data); } - 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; + if (data.content.membership === 'join') { + return membershipJoinAction(data); } - // 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; - } + logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); - // 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}`); - } } catch (error) { logger.error('Failed to process Matrix membership event:', error); } From 6370679d217b9edee60d757f4dc0545b6ca82782 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 16:44:05 -0300 Subject: [PATCH 96/99] fix: non authorized endpoints requiring authorization --- ee/packages/federation-matrix/src/FederationMatrix.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 6e7e687da847e..f9efe0fd032f0 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -184,14 +184,14 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS 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(getKeyServerRoutes(this.homeserverServices)) - .use(getFederationVersionsRoutes(this.homeserverServices)) .use(getMatrixMediaRoutes(this.homeserverServices)); wellKnown.use(isFederationEnabledMiddleware).use(isLicenseEnabledMiddleware).use(getWellKnownRoutes(this.homeserverServices)); From b96cff4352f983e2ad8a6add3563d5ac7a0deac1 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 19:01:09 -0300 Subject: [PATCH 97/99] bump: @rocket.chat/federation-sdk --- ee/apps/federation-service/package.json | 2 +- ee/packages/federation-matrix/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 6b806cba22833..744074f2e8fc3 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/core-typings": "workspace:*", "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", - "@rocket.chat/federation-sdk": "0.1.7", + "@rocket.chat/federation-sdk": "0.1.8", "@rocket.chat/http-router": "workspace:*", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/license": "workspace:^", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 5998f5994b39b..5d26e503b52c9 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -38,7 +38,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.1.7", + "@rocket.chat/federation-sdk": "0.1.8", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 2fa9bd7b25b4c..24daf7f32c5c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7543,7 +7543,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.7" + "@rocket.chat/federation-sdk": "npm:0.1.8" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7569,9 +7569,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.1.7": - version: 0.1.7 - resolution: "@rocket.chat/federation-sdk@npm:0.1.7" +"@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" @@ -7584,7 +7584,7 @@ __metadata: zod: "npm:^3.22.4" peerDependencies: typescript: ~5.9.2 - checksum: 10/06ce073279e01b629b4ddac301dbf67fff9521046d6586c9e0799df194a15d1ab69be1e3ea0820804e376cecc0c2855327c3307ae985faec5540047d4dac09e6 + checksum: 10/8a3d05a82ced70d462527514a9ea8ba8469635f085b88ef568a6ccabfcd77a4dd979d610b56fe263fc3915a073d49585ac52863fd14a98aae5a89ebdc605bf5b languageName: node linkType: hard @@ -7597,7 +7597,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:*" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.7" + "@rocket.chat/federation-sdk": "npm:0.1.8" "@rocket.chat/http-router": "workspace:*" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/license": "workspace:^" From 51acb08e73dda05ca97a17ed4a8ec3d971188d15 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 19:29:25 -0300 Subject: [PATCH 98/99] fix sending thumbs --- .../federation-matrix/src/FederationMatrix.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index f9efe0fd032f0..ea0f33eb5dfb6 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -427,22 +427,23 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS try { let lastEventId: { eventId: string } | null = null; - for await (const file of 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, - }, - }; + // 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); - } + lastEventId = await this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId); return lastEventId; } catch (error) { From 47f9d7e0453c27f02290871afdb4e49014efceff Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 25 Sep 2025 19:29:32 -0300 Subject: [PATCH 99/99] fix lint --- ee/packages/federation-matrix/src/events/member.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index 89787f46a3a61..efa6c865aad0d 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -96,7 +96,6 @@ export function member(emitter: Emitter) { } logger.debug(`Ignoring membership event with membership: ${data.content.membership}`); - } catch (error) { logger.error('Failed to process Matrix membership event:', error); }