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; }; }