snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
322
apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts
Normal file
322
apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
|
||||
import type { IMessage, IRoom, IUser, MessageAttachment, FileProp, RoomType, IExportOperation } from '@rocket.chat/core-typings';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { escapeHTML } from '@rocket.chat/tools';
|
||||
|
||||
import { readSecondaryPreferred } from '../../database/readSecondaryPreferred';
|
||||
import { settings } from '../../settings';
|
||||
import { joinPath } from '../fileUtils';
|
||||
import { i18n } from '../i18n';
|
||||
|
||||
const hideUserName = (username: string, userData: Pick<IUser, 'username'> | undefined, usersMap: Record<string, string>) => {
|
||||
if (!usersMap[username]) {
|
||||
if (userData && username === userData.username) {
|
||||
usersMap[username] = username;
|
||||
} else {
|
||||
usersMap[username] = `User_${Object.keys(usersMap).length + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
return usersMap[username];
|
||||
};
|
||||
|
||||
const getAttachmentData = (attachment: MessageAttachment, message: IMessage) => {
|
||||
return {
|
||||
type: 'type' in attachment ? attachment.type : undefined,
|
||||
title: attachment.title,
|
||||
title_link: attachment.title_link,
|
||||
image_url: 'image_url' in attachment ? attachment.image_url : undefined,
|
||||
audio_url: 'audio_url' in attachment ? attachment.audio_url : undefined,
|
||||
video_url: 'video_url' in attachment ? attachment.video_url : undefined,
|
||||
message_link: 'message_link' in attachment ? attachment.message_link : undefined,
|
||||
image_type: 'image_type' in attachment ? attachment.image_type : undefined,
|
||||
image_size: 'image_size' in attachment ? attachment.image_size : undefined,
|
||||
video_size: 'video_size' in attachment ? attachment.video_size : undefined,
|
||||
video_type: 'video_type' in attachment ? attachment.video_type : undefined,
|
||||
audio_size: 'audio_size' in attachment ? attachment.audio_size : undefined,
|
||||
audio_type: 'audio_type' in attachment ? attachment.audio_type : undefined,
|
||||
url:
|
||||
attachment.title_link ||
|
||||
('image_url' in attachment ? attachment.image_url : undefined) ||
|
||||
('audio_url' in attachment ? attachment.audio_url : undefined) ||
|
||||
('video_url' in attachment ? attachment.video_url : undefined) ||
|
||||
('message_link' in attachment ? attachment.message_link : undefined) ||
|
||||
null,
|
||||
remote: !message.file?._id,
|
||||
fileId: message.file?._id,
|
||||
fileName: message.file?.name,
|
||||
};
|
||||
};
|
||||
|
||||
export type MessageData = Pick<IMessage, 'msg' | 'ts'> & {
|
||||
username?: IUser['username'];
|
||||
attachments?: ReturnType<typeof getAttachmentData>[];
|
||||
type?: IMessage['t'];
|
||||
};
|
||||
|
||||
export const getMessageData = (
|
||||
msg: IMessage,
|
||||
hideUsers: boolean,
|
||||
userData: Pick<IUser, 'username'> | undefined,
|
||||
usersMap: IExportOperation['userNameTable'],
|
||||
): MessageData => {
|
||||
const username = hideUsers ? hideUserName(msg.u.username || msg.u.name || '', userData, usersMap) : msg.u.username;
|
||||
|
||||
const messageObject = {
|
||||
msg: msg.msg,
|
||||
username,
|
||||
ts: msg.ts,
|
||||
...(msg.attachments && {
|
||||
attachments: msg.attachments.map((attachment) => getAttachmentData(attachment, msg)),
|
||||
}),
|
||||
...(msg.t && { type: msg.t }),
|
||||
};
|
||||
|
||||
switch (msg.t) {
|
||||
case 'uj':
|
||||
messageObject.msg = i18n.t('User_joined_the_channel');
|
||||
break;
|
||||
case 'ul':
|
||||
messageObject.msg = i18n.t('User_left_this_channel');
|
||||
break;
|
||||
case 'ui':
|
||||
messageObject.msg = i18n.t('User_invited_to_room', {
|
||||
user_invited: hideUserName(msg.msg, userData, usersMap),
|
||||
});
|
||||
break;
|
||||
case 'uir':
|
||||
messageObject.msg = i18n.t('User_rejected_invitation_to_room');
|
||||
break;
|
||||
case 'ult':
|
||||
messageObject.msg = i18n.t('User_left_this_team');
|
||||
break;
|
||||
case 'user-added-room-to-team':
|
||||
messageObject.msg = i18n.t('added__roomName__to_this_team', {
|
||||
roomName: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'user-converted-to-team':
|
||||
messageObject.msg = i18n.t('Converted__roomName__to_a_team', {
|
||||
roomName: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'user-converted-to-channel':
|
||||
messageObject.msg = i18n.t('Converted__roomName__to_a_channel', {
|
||||
roomName: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'user-deleted-room-from-team':
|
||||
messageObject.msg = i18n.t('Deleted__roomName__room', {
|
||||
roomName: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'user-removed-room-from-team':
|
||||
messageObject.msg = i18n.t('Removed__roomName__from_the_team', {
|
||||
roomName: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'ujt':
|
||||
messageObject.msg = i18n.t('User_joined_the_team');
|
||||
break;
|
||||
case 'au':
|
||||
messageObject.msg = i18n.t('User_added_to', {
|
||||
user_added: hideUserName(msg.msg, userData, usersMap),
|
||||
user_by: username,
|
||||
});
|
||||
break;
|
||||
case 'added-user-to-team':
|
||||
messageObject.msg = i18n.t('Added__username__to_this_team', {
|
||||
user_added: msg.msg,
|
||||
});
|
||||
break;
|
||||
case 'r':
|
||||
messageObject.msg = i18n.t('Room_name_changed_to', {
|
||||
room_name: msg.msg,
|
||||
user_by: username,
|
||||
});
|
||||
break;
|
||||
case 'ru':
|
||||
messageObject.msg = i18n.t('User_has_been_removed', {
|
||||
user_removed: hideUserName(msg.msg, userData, usersMap),
|
||||
user_by: username,
|
||||
});
|
||||
break;
|
||||
case 'removed-user-from-team':
|
||||
messageObject.msg = i18n.t('Removed__username__from_the_team', {
|
||||
user_removed: hideUserName(msg.msg, userData, usersMap),
|
||||
});
|
||||
break;
|
||||
case 'wm':
|
||||
messageObject.msg = i18n.t('Welcome', { interpolation: { escapeValue: false }, user: username });
|
||||
break;
|
||||
case 'livechat-close':
|
||||
messageObject.msg = i18n.t('Conversation_finished');
|
||||
break;
|
||||
case 'livechat-started':
|
||||
messageObject.msg = i18n.t('Chat_started');
|
||||
break;
|
||||
case 'abac-removed-user-from-room':
|
||||
messageObject.msg = i18n.t('abac_removed_user_from_the_room');
|
||||
break;
|
||||
}
|
||||
|
||||
return messageObject;
|
||||
};
|
||||
|
||||
export const exportMessageObject = (type: 'json' | 'html', messageObject: MessageData, messageFiles: FileProp[] = []): string => {
|
||||
if (type === 'json') {
|
||||
return JSON.stringify(messageObject);
|
||||
}
|
||||
|
||||
const file = [];
|
||||
|
||||
const messageType = messageObject.type;
|
||||
const timestamp = messageObject.ts ? new Date(messageObject.ts).toUTCString() : '';
|
||||
|
||||
const italicTypes: IMessage['t'][] = ['uj', 'ul', 'au', 'r', 'ru', 'wm', 'livechat-close'];
|
||||
|
||||
const safeMsg = escapeHTML(messageObject.msg ?? '');
|
||||
const message = italicTypes.includes(messageType) ? `<i>${safeMsg}</i>` : safeMsg;
|
||||
|
||||
file.push(`<p><strong>${escapeHTML(messageObject.username ?? '')}</strong> (${timestamp}):<br/>`);
|
||||
file.push(message);
|
||||
|
||||
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 assetUrl = `./assets/${messageFile._id}-${messageFile.name}`;
|
||||
const link = `<br/><a href="${escapeHTML(assetUrl)}">${escapeHTML(description)}</a>`;
|
||||
file.push(link);
|
||||
}
|
||||
}
|
||||
|
||||
file.push('</p>');
|
||||
|
||||
return file.join('\n');
|
||||
};
|
||||
|
||||
export const exportRoomMessages = async (
|
||||
rid: IRoom['_id'],
|
||||
exportType: 'json' | 'html',
|
||||
skip: number,
|
||||
limit: number,
|
||||
userData: any,
|
||||
filter: any = {},
|
||||
usersMap: IExportOperation['userNameTable'] = {},
|
||||
hideUsers = true,
|
||||
) => {
|
||||
const readPreference = readSecondaryPreferred();
|
||||
|
||||
const { cursor, totalCount } = Messages.findPaginated(
|
||||
{ ...filter, rid },
|
||||
{
|
||||
sort: { ts: 1 },
|
||||
skip,
|
||||
limit,
|
||||
readPreference,
|
||||
},
|
||||
);
|
||||
|
||||
const [results, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
const result = {
|
||||
total,
|
||||
exported: results.length,
|
||||
messages: [] as string[],
|
||||
uploads: [] as FileProp[],
|
||||
};
|
||||
|
||||
results.forEach((msg) => {
|
||||
const messageObject = getMessageData(msg, hideUsers, userData, usersMap);
|
||||
|
||||
// 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.uploads.push(...files);
|
||||
result.messages.push(exportMessageObject(exportType, messageObject, files));
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const exportRoomMessagesToFile = async function (
|
||||
exportPath: string,
|
||||
assetsPath: string,
|
||||
exportType: 'json' | 'html',
|
||||
roomList: (
|
||||
| {
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
userId: string | undefined;
|
||||
exportedCount: number;
|
||||
status: string;
|
||||
type: RoomType;
|
||||
targetFile: string;
|
||||
}
|
||||
| Record<string, never>
|
||||
)[],
|
||||
userData: IUser,
|
||||
messagesFilter = {},
|
||||
usersMap: IExportOperation['userNameTable'] = {},
|
||||
hideUsers = true,
|
||||
) {
|
||||
await mkdir(exportPath, { recursive: true });
|
||||
await mkdir(assetsPath, { recursive: true });
|
||||
|
||||
const result = {
|
||||
fileList: [] as FileProp[],
|
||||
};
|
||||
|
||||
const limit =
|
||||
settings.get<number>('UserData_MessageLimitPerRequest') > 0 ? settings.get<number>('UserData_MessageLimitPerRequest') : 1000;
|
||||
for await (const exportOpRoomData of roomList) {
|
||||
if (!('targetFile' in exportOpRoomData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = joinPath(exportPath, exportOpRoomData.targetFile);
|
||||
if (exportOpRoomData.status === 'pending') {
|
||||
exportOpRoomData.status = 'exporting';
|
||||
if (exportType === 'html') {
|
||||
await writeFile(
|
||||
filePath,
|
||||
[
|
||||
'<meta http-equiv="content-type" content="text/html; charset=utf-8">',
|
||||
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:;">`,
|
||||
].join('\n'),
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const skip = exportOpRoomData.exportedCount;
|
||||
|
||||
const { total, exported, uploads, messages } = await exportRoomMessages(
|
||||
exportOpRoomData.roomId,
|
||||
exportType,
|
||||
skip,
|
||||
limit,
|
||||
userData,
|
||||
messagesFilter,
|
||||
usersMap,
|
||||
hideUsers,
|
||||
);
|
||||
|
||||
result.fileList.push(...uploads);
|
||||
|
||||
exportOpRoomData.exportedCount += exported;
|
||||
|
||||
if (total <= exportOpRoomData.exportedCount) {
|
||||
exportOpRoomData.status = 'completed';
|
||||
}
|
||||
|
||||
await writeFile(filePath, `${messages.join('\n')}\n`, { encoding: 'utf8', flag: 'a' });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user