import { mkdir, writeFile } from 'fs/promises'; import type { IMessage, IRoom, IUser, MessageAttachment, FileProp, RoomType, IExportOperation } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; import { readSecondaryPreferred } from '../../database/readSecondaryPreferred'; import { joinPath } from '../fileUtils'; import { i18n } from '../i18n'; const hideUserName = (username: string, userData: Pick | undefined, usersMap: Record) => { 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 & { username?: IUser['username'] | IUser['name']; attachments?: ReturnType[]; type?: IMessage['t']; }; export const getMessageData = ( msg: IMessage, hideUsers: boolean, userData: Pick | 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', { 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, messageFile?: 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 message = italicTypes.includes(messageType) ? `${messageObject.msg}` : messageObject.msg; file.push(`

${messageObject.username} (${timestamp}):
`); file.push(message); 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 = `
${description}`; file.push(link); } file.push('

'); 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); if (msg.file) { result.uploads.push(msg.file); } result.messages.push(exportMessageObject(exportType, messageObject, msg.file)); }); 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 )[], 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('UserData_MessageLimitPerRequest') > 0 ? settings.get('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, '', { 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; };