Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/quick-schools-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes room message export to correctly handle messages with multiple files.
25 changes: 14 additions & 11 deletions apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const getMessageData = (
return messageObject;
};

export const exportMessageObject = (type: 'json' | 'html', messageObject: MessageData, messageFile?: FileProp): string => {
export const exportMessageObject = (type: 'json' | 'html', messageObject: MessageData, messageFiles: FileProp[] = []): string => {
if (type === 'json') {
return JSON.stringify(messageObject);
}
Expand All @@ -180,14 +180,16 @@ export const exportMessageObject = (type: 'json' | 'html', messageObject: Messag
file.push(`<p><strong>${messageObject.username}</strong> (${timestamp}):<br/>`);
file.push(message);

if (messageFile?._id) {
const attachment = messageObject.attachments?.find((att) => att.type === 'file' && att.title_link?.includes(messageFile._id));
for (const messageFile of messageFiles) {
if (messageFile?._id) {
const attachment = messageObject.attachments?.find((att) => att.type === 'file' && att.title_link?.includes(messageFile._id));

const description = attachment?.title || i18n.t('Message_Attachments');
const description = attachment?.title || i18n.t('Message_Attachments');

const assetUrl = `./assets/${messageFile._id}-${messageFile.name}`;
const link = `<br/><a href="${assetUrl}">${description}</a>`;
file.push(link);
const assetUrl = `./assets/${messageFile._id}-${messageFile.name}`;
const link = `<br/><a href="${assetUrl}">${description}</a>`;
file.push(link);
}
}

file.push('</p>');
Expand Down Expand Up @@ -229,11 +231,12 @@ export const exportRoomMessages = async (
results.forEach((msg) => {
const messageObject = getMessageData(msg, hideUsers, userData, usersMap);

if (msg.file) {
result.uploads.push(msg.file);
}
// handle both new format (msg.files array) and old format (msg.file) for backward compatibility
// and filter out thumbnails (typeGroup === 'thumb') to only include actual files
const files = (msg.files || (msg.file ? [msg.file] : [])).filter((file) => file && file.typeGroup !== 'thumb');

result.messages.push(exportMessageObject(exportType, messageObject, msg.file));
result.uploads.push(...files);
result.messages.push(exportMessageObject(exportType, messageObject, files));
Comment thread
pierre-lehnen-rc marked this conversation as resolved.
});

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('Export - exportMessageObject', () => {
});

it('should correctly reference file when exporting a message object with an attachment as html', async () => {
const result = await exportMessageObject('html', messagesData[1], exportMessagesMock[1].file);
const result = await exportMessageObject('html', messagesData[1], [exportMessagesMock[1].file]);

expect(result).to.be.a.string;
expect(result).to.equal(
Expand All @@ -100,7 +100,7 @@ describe('Export - exportMessageObject', () => {
});

it('should use fallback attachment description when no title is provided on message object export as html', async () => {
const result = await exportMessageObject('html', messagesData[2], exportMessagesMock[2].file);
const result = await exportMessageObject('html', messagesData[2], [exportMessagesMock[2].file]);

expect(stubs.translateKey.calledWith('Message_Attachments')).to.be.true;
expect(result).to.be.a.string;
Expand Down Expand Up @@ -155,4 +155,56 @@ describe('Export - exportRoomMessages', () => {
const messagesWithFiles = exportMessagesMock.filter((message) => message.file);
expect(result).to.have.property('uploads').that.is.an('array').of.length(messagesWithFiles.length);
});

it('should export multiple files and filter out thumbnails', async () => {
const message = {
_id: faker.database.mongodbObjectId(),
rid: 'test-rid',
ts: new Date(),
u: { _id: faker.database.mongodbObjectId(), username: 'testuser' },
msg: 'Message with files',
files: [
{ _id: 'file-1', name: 'photo.jpg', type: 'image/jpeg', size: 500000 },
{ _id: 'file-2', name: 'doc.pdf', type: 'application/pdf', size: 10000 },
{ _id: 'thumb-1', name: 'photo_thumb.jpg', type: 'image/jpeg', size: 5000, typeGroup: 'thumb' },
],
attachments: [{ type: 'file', title: 'photo.jpg', title_link: '/file-upload/file-1/photo.jpg' }],
};

stubs.findPaginatedMessagesCursor.resolves([message]);
stubs.findPaginatedMessagesTotal.resolves(1);
stubs.findPaginatedMessages.returns({
cursor: { toArray: stubs.findPaginatedMessagesCursor },
totalCount: stubs.findPaginatedMessagesTotal(),
});

const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData);

expect(result.uploads).to.have.length(2);
expect(result.uploads.some((f: { typeGroup?: string }) => f.typeGroup === 'thumb')).to.be.false;
});

it('should fallback to msg.file when msg.files is not available', async () => {
const message = {
_id: faker.database.mongodbObjectId(),
rid: 'test-rid',
ts: new Date(),
u: { _id: faker.database.mongodbObjectId(), username: 'testuser' },
msg: 'Old format message',
file: { _id: 'single-file', name: 'doc.pdf', type: 'application/pdf', size: 10000 },
attachments: [{ type: 'file', title: 'doc.pdf', title_link: '/file-upload/single-file/doc.pdf' }],
};

stubs.findPaginatedMessagesCursor.resolves([message]);
stubs.findPaginatedMessagesTotal.resolves(1);
stubs.findPaginatedMessages.returns({
cursor: { toArray: stubs.findPaginatedMessagesCursor },
totalCount: stubs.findPaginatedMessagesTotal(),
});

const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData);

expect(result.uploads).to.have.length(1);
expect(result.uploads[0]._id).to.equal('single-file');
});
});
Loading