Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,7 +52,7 @@ callbacks.add(
}
},
callbacks.priority.HIGH,
'federation-v2-after-room-message-sent',
'native-federation-after-room-message-sent',
);

callbacks.add(
Expand Down Expand Up @@ -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,
);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/lib/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/server/services/messages/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
20 changes: 0 additions & 20 deletions ee/apps/federation-service/src/config.ts
Original file line number Diff line number Diff line change
@@ -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',
};
40 changes: 23 additions & 17 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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('@')) {
Expand Down Expand Up @@ -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) {
Comment on lines +433 to 453
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Multi-file messages lose all but last Matrix event ID.

handleFileMessage returns only the last eventId, and we persist a single federation.eventId. Deletions/edits will miss earlier file events.

  • Return all event IDs from handleFileMessage (e.g., eventIds: string[]) and persist them (new field or mapping table).
  • Update deletion/redaction paths to iterate all event IDs.

If schema changes aren’t feasible now, gate multi-file sending or send a single aggregated file until persistence supports multiples.

Also applies to: 571-571

this.logger.error('Failed to handle file message', {
messageId: message._id,
Expand Down
8 changes: 3 additions & 5 deletions ee/packages/federation-matrix/src/api/_matrix/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions ee/packages/federation-matrix/src/api/_matrix/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
required: ['roomId', 'userId'],
};

// @ts-ignore

Check warning on line 152 in ee/packages/federation-matrix/src/api/_matrix/profiles.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isMakeJoinParamsProps = ajv.compile(MakeJoinParamsSchema);

Expand All @@ -167,7 +167,7 @@
},
};

// @ts-ignore

Check warning on line 170 in ee/packages/federation-matrix/src/api/_matrix/profiles.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isMakeJoinQueryProps = ajv.compile(MakeJoinQuerySchema);

Expand Down Expand Up @@ -253,7 +253,7 @@
required: ['room_version', 'event'],
};

// @ts-ignore

Check warning on line 256 in ee/packages/federation-matrix/src/api/_matrix/profiles.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isMakeJoinResponseProps = ajv.compile(MakeJoinResponseSchema);

Expand Down Expand Up @@ -421,11 +421,10 @@
.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'],
Expand Down
7 changes: 4 additions & 3 deletions ee/packages/federation-matrix/src/api/_matrix/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})
Expand Down
6 changes: 3 additions & 3 deletions ee/packages/federation-matrix/src/api/_matrix/send-join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
required: ['roomId', 'stateKey'],
};

// @ts-ignore

Check warning on line 50 in ee/packages/federation-matrix/src/api/_matrix/send-join.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema);

Expand Down Expand Up @@ -184,7 +184,7 @@
],
};

// @ts-ignore

Check warning on line 187 in ee/packages/federation-matrix/src/api/_matrix/send-join.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isSendJoinEventProps = ajv.compile(SendJoinEventSchema);

Expand Down Expand Up @@ -218,7 +218,7 @@
required: ['event', 'state', 'auth_chain', 'members_omitted', 'origin'],
};

// @ts-ignore

Check warning on line 221 in ee/packages/federation-matrix/src/api/_matrix/send-join.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isSendJoinResponseProps = ajv.compile(SendJoinResponseSchema);

Expand All @@ -228,10 +228,10 @@
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'],
Expand Down
95 changes: 43 additions & 52 deletions ee/packages/federation-matrix/src/events/message.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,40 +25,37 @@ 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;
rid: string;
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';
Expand All @@ -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],
};
}
Expand All @@ -124,8 +116,8 @@ export function message(emitter: Emitter<HomeserverEventSignatures>, 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');
Comment on lines +119 to 123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Possible TypeError: content.body may be undefined.

Guard before .toString().

-const messageBody = content.body.toString();
+const messageBody = content.body != null ? String(content.body) : '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { msgtype } = content;
const messageBody = content.body.toString();
if (!messageBody && !msgtype) {
logger.debug('No message content found in event');
const { msgtype } = content;
const messageBody = content.body != null ? String(content.body) : '';
if (!messageBody && !msgtype) {
logger.debug('No message content found in event');
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 193 to 197,
the code calls content.body.toString() but content.body can be undefined which
will throw a TypeError; guard access before calling toString by first checking
whether content.body is defined (and optionally whether it's a string/Buffer),
e.g. compute messageBody only if content.body != null (or use optional
chaining/ternary to default to an empty string), move the !messageBody &&
!msgtype check after this safe extraction, and ensure any logging or downstream
logic handles the default empty string case.

Expand All @@ -152,8 +144,6 @@ export function message(emitter: Emitter<HomeserverEventSignatures>, 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');
Expand Down Expand Up @@ -236,8 +226,9 @@ export function message(emitter: Emitter<HomeserverEventSignatures>, 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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading