From 4ee6d2fafcaf440657debb9ada0e1a691ed414fb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Apr 2024 18:16:10 -0300 Subject: [PATCH 001/112] POC --- apps/meteor/app/e2e/client/helper.js | 8 ++ .../app/e2e/client/rocketchat.e2e.room.js | 42 +++++----- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 +- .../server/methods/sendFileMessage.ts | 79 +++++++++++-------- .../message/content/Attachments.tsx | 4 +- .../content/attachments/AttachmentsItem.tsx | 4 +- .../attachments/file/ImageAttachment.tsx | 2 + .../attachments/structure/AttachmentImage.tsx | 53 ++++++++++++- .../variants/room/RoomMessageContent.tsx | 7 +- .../client/lib/chats/flows/uploadFiles.ts | 13 ++- 10 files changed, 151 insertions(+), 65 deletions(-) diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.js index 2e0843ee3380..49b157c5ccf4 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.js @@ -53,6 +53,10 @@ export async function encryptAES(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } +export async function encryptAESCTR(vector, key, data) { + return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); +} + export async function decryptRSA(key, data) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } @@ -65,6 +69,10 @@ export async function generateAESKey() { return crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); } +export async function generateAESCTRKey() { + return crypto.subtle.generateKey({ name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']); +} + export async function generateRSAKey() { return crypto.subtle.generateKey( { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 9e2f72d38115..aafb2c8b99c7 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -23,6 +23,8 @@ import { importAESKey, importRSAKey, readFileAsArrayBuffer, + encryptAESCTR, + generateAESCTRKey, } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; @@ -335,42 +337,40 @@ export class E2ERoom extends Emitter { // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return; - } + // if (!this.isSupportedRoomType(this.typeOfRoom)) { + // return; + // } const fileArrayBuffer = await readFileAsArrayBuffer(file); const vector = crypto.getRandomValues(new Uint8Array(16)); + const key = await generateAESCTRKey(); let result; try { - result = await encryptAES(vector, this.groupSessionKey, fileArrayBuffer); + result = await encryptAESCTR(vector, key, fileArrayBuffer); } catch (error) { + console.log(error); return this.error('Error encrypting group key: ', error); } - const output = joinVectorAndEcryptedData(vector, result); + const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const encryptedFile = new File([toArrayBuffer(EJSON.stringify(output))], file.name); + const encryptedFile = new File([toArrayBuffer(result)], file.name); - return encryptedFile; + return { + file: encryptedFile, + key: exportedKey, + iv: Base64.encode(vector), + type: file.type, + }; } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(message) { - if (message[0] !== '{') { - return; - } + async decryptFile(file, key, iv) { + const ivArray = Base64.decode(iv); + const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(message)); - - try { - return await decryptAES(vector, this.groupSessionKey, cipherText); - } catch (error) { - this.error('Error decrypting file: ', error); - - return false; - } + return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); } // Encrypts messages @@ -440,7 +440,7 @@ export class E2ERoom extends Emitter { return { ...message, msg: data.text, - e2e: 'done', + // e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 472e71959933..57944ddd0297 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,7 @@ class E2E extends Emitter { ...message, ...(data && { msg: data.text, - e2e: 'done', + // e2e: 'done', }), }; @@ -474,7 +474,7 @@ class E2E extends Emitter { return { ...message, attachments: decryptedAttachments, - e2e: 'done', + // e2e: 'done', }; } diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 08e3225ad9a1..6234090371f6 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -33,6 +33,7 @@ export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, user: IUser, + msgData?: Record, ): Promise => { validateFileRequiredFields(file); @@ -42,6 +43,16 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; + let e2e; + console.log(msgData?.e2e); + if (msgData?.e2e) { + e2e = JSON.parse(msgData.e2e); + } + + if (e2e.type) { + file.type = e2e.type; + } + const files = [ { _id: file._id, @@ -68,39 +79,39 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); - } - } catch (e) { - SystemLogger.error(e); - } + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // } + // } catch (e) { + // SystemLogger.error(e); + // } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { @@ -191,7 +202,7 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user, msgData); const msg = await executeSendMessage(userId, { rid: roomId, diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index dea78a3f6513..02de21e120aa 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -11,12 +11,12 @@ type AttachmentsProps = { isMessageEncrypted?: boolean; }; -const Attachments = ({ attachments, id, isMessageEncrypted = false }: AttachmentsProps): ReactElement => { +const Attachments = ({ attachments, id, isMessageEncrypted = false, message }: AttachmentsProps): ReactElement => { return ( <> {attachments?.map((attachment, index) => ( - + ))} diff --git a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx index dd4f9bac9d28..9adf6b29f87f 100644 --- a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx +++ b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx @@ -12,9 +12,9 @@ type AttachmentsItemProps = { id: string | undefined; }; -const AttachmentsItem = ({ attachment, id }: AttachmentsItemProps): ReactElement => { +const AttachmentsItem = ({ attachment, id, message }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment)) { - return ; + return ; } if (isQuoteAttachment(attachment)) { diff --git a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx index 819d6581b314..81d06f4d9bd2 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -22,6 +22,7 @@ const ImageAttachment = ({ title_link: link, title_link_download: hasDownload, collapsed, + message, }: ImageAttachmentProps) => { const [loadImage, setLoadImage] = useLoadImage(); const getURL = useMediaUrl(); @@ -38,6 +39,7 @@ const ImageAttachment = ({ src={getURL(url)} previewUrl={`data:image/png;base64,${imagePreview}`} id={id} + message={message} /> diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 5b251ad6143f..0c4c6c9db657 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -1,9 +1,11 @@ import type { Dimensions } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useAttachmentDimensions } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { FC } from 'react'; import React, { memo, useState, useMemo } from 'react'; +import { e2e } from '../../../../../../app/e2e/client'; import ImageBox from './image/ImageBox'; import Load from './image/Load'; import Retry from './image/Retry'; @@ -37,7 +39,7 @@ const getDimensions = ( return { width, height, ratio: (height / width) * 100 }; }; -const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { +const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, message, ...size }) => { const limits = useAttachmentDimensions(); const [error, setError] = useState(false); @@ -51,6 +53,53 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo [], ); + async function imageUrlToBase64(blob: Blob) { + return new Promise((onSuccess, onError) => { + try { + const reader = new FileReader(); + reader.onload = function () { + onSuccess(this.result); + }; + reader.readAsDataURL(blob); + } catch (e) { + onError(e); + } + }); + } + + async function imageUrlArrayBuffer(blob: Blob) { + return new Promise((onSuccess, onError) => { + try { + const reader = new FileReader(); + reader.onload = function () { + onSuccess(this.result); + }; + reader.readAsArrayBuffer(blob); + } catch (e) { + onError(e); + } + }); + } + + const base64 = useQuery( + [src], + async () => { + const file = await fetch(src); + const blob = await file.blob(); + if (message.e2e) { + const { key, iv } = JSON.parse(message.e2e); + const e2eRoom = await e2e.getInstanceByRoomId(message.rid); + + const file = await e2eRoom.decryptFile(await imageUrlArrayBuffer(blob), key, iv); + return imageUrlToBase64(new Blob([file])); + } + return imageUrlToBase64(blob); + }, + { + suspense: true, + }, + ); + const dimensions = getDimensions(width, height, limits); const background = previewUrl && `url(${previewUrl}) center center / cover no-repeat fixed`; @@ -82,7 +131,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo data-id={id} className='gallery-item' data-src={dataSrc || src} - src={src} + src={base64.data} alt='' width={dimensions.width} height={dimensions.height} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4bd548393d02..146c9ac72bb2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -68,7 +68,12 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {!!normalizedMessage?.attachments?.length && ( - + )} {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 801ac2aa883c..fe4c4832e668 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -70,7 +70,18 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedDescription = await e2eRoom.encryptAttachmentDescription(description, Random.id()); - uploadFile(file, encryptedDescription, { t: 'e2e', e2e: 'pending' }); + const encryptedFile = await e2eRoom.encryptFile(file); + + if (encryptedFile) { + uploadFile(encryptedFile.file, encryptedDescription, { + t: 'e2e', + e2e: JSON.stringify({ + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }), + }); + } }, invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), }, From a1c05694ecee8e5b7d41dde49885e84093022563 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Apr 2024 21:25:19 -0300 Subject: [PATCH 002/112] Initial service worker implementation --- .../message/content/Attachments.tsx | 4 +- .../content/attachments/AttachmentsItem.tsx | 4 +- .../attachments/file/ImageAttachment.tsx | 2 - .../attachments/structure/AttachmentImage.tsx | 53 +----------------- .../variants/room/RoomMessageContent.tsx | 28 ++++++++-- .../client/lib/chats/flows/uploadFiles.ts | 13 +++++ apps/meteor/public/enc.js | 55 +++++++++++++++++++ 7 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 apps/meteor/public/enc.js diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index 02de21e120aa..dea78a3f6513 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -11,12 +11,12 @@ type AttachmentsProps = { isMessageEncrypted?: boolean; }; -const Attachments = ({ attachments, id, isMessageEncrypted = false, message }: AttachmentsProps): ReactElement => { +const Attachments = ({ attachments, id, isMessageEncrypted = false }: AttachmentsProps): ReactElement => { return ( <> {attachments?.map((attachment, index) => ( - + ))} diff --git a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx index 9adf6b29f87f..dd4f9bac9d28 100644 --- a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx +++ b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx @@ -12,9 +12,9 @@ type AttachmentsItemProps = { id: string | undefined; }; -const AttachmentsItem = ({ attachment, id, message }: AttachmentsItemProps): ReactElement => { +const AttachmentsItem = ({ attachment, id }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment)) { - return ; + return ; } if (isQuoteAttachment(attachment)) { diff --git a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx index 81d06f4d9bd2..819d6581b314 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -22,7 +22,6 @@ const ImageAttachment = ({ title_link: link, title_link_download: hasDownload, collapsed, - message, }: ImageAttachmentProps) => { const [loadImage, setLoadImage] = useLoadImage(); const getURL = useMediaUrl(); @@ -39,7 +38,6 @@ const ImageAttachment = ({ src={getURL(url)} previewUrl={`data:image/png;base64,${imagePreview}`} id={id} - message={message} /> diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 0c4c6c9db657..5b251ad6143f 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -1,11 +1,9 @@ import type { Dimensions } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useAttachmentDimensions } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; import type { FC } from 'react'; import React, { memo, useState, useMemo } from 'react'; -import { e2e } from '../../../../../../app/e2e/client'; import ImageBox from './image/ImageBox'; import Load from './image/Load'; import Retry from './image/Retry'; @@ -39,7 +37,7 @@ const getDimensions = ( return { width, height, ratio: (height / width) * 100 }; }; -const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, message, ...size }) => { +const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { const limits = useAttachmentDimensions(); const [error, setError] = useState(false); @@ -53,53 +51,6 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo [], ); - async function imageUrlToBase64(blob: Blob) { - return new Promise((onSuccess, onError) => { - try { - const reader = new FileReader(); - reader.onload = function () { - onSuccess(this.result); - }; - reader.readAsDataURL(blob); - } catch (e) { - onError(e); - } - }); - } - - async function imageUrlArrayBuffer(blob: Blob) { - return new Promise((onSuccess, onError) => { - try { - const reader = new FileReader(); - reader.onload = function () { - onSuccess(this.result); - }; - reader.readAsArrayBuffer(blob); - } catch (e) { - onError(e); - } - }); - } - - const base64 = useQuery( - [src], - async () => { - const file = await fetch(src); - const blob = await file.blob(); - if (message.e2e) { - const { key, iv } = JSON.parse(message.e2e); - const e2eRoom = await e2e.getInstanceByRoomId(message.rid); - - const file = await e2eRoom.decryptFile(await imageUrlArrayBuffer(blob), key, iv); - return imageUrlToBase64(new Blob([file])); - } - return imageUrlToBase64(blob); - }, - { - suspense: true, - }, - ); - const dimensions = getDimensions(width, height, limits); const background = previewUrl && `url(${previewUrl}) center center / cover no-repeat fixed`; @@ -131,7 +82,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo data-id={id} className='gallery-item' data-src={dataSrc || src} - src={base64.data} + src={src} alt='' width={dimensions.width} height={dimensions.height} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 146c9ac72bb2..5bc16ddfdf43 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,3 +1,4 @@ +import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; @@ -46,6 +47,26 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + if (normalizedMessage?.attachments?.length) { + normalizedMessage.attachments.forEach((attachment) => { + if (!normalizedMessage.e2e) { + return; + } + console.log(attachment); + + const key = Base64.encode(normalizedMessage.e2e); + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (attachment.image_url && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (attachment.audio_url && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + }); + } + return ( <> {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( @@ -68,12 +89,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {!!normalizedMessage?.attachments?.length && ( - + )} {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fe4c4832e668..7ee613eb23f1 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -9,6 +9,19 @@ import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; +if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/enc.js', { + scope: '/', + }) + .then((reg) => { + if (reg.active) console.log('service worker installed'); + }) + .catch((err) => { + console.log(`registration failed: ${err}`); + }); +} + export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js new file mode 100644 index 000000000000..25b74110b11a --- /dev/null +++ b/apps/meteor/public/enc.js @@ -0,0 +1,55 @@ +function base64Decode (string) { + string = atob(string); + const + length = string.length, + buf = new ArrayBuffer(length), + bufView = new Uint8Array(buf); + for (var i = 0; i < string.length; i++) { bufView[i] = string.charCodeAt(i) } + return buf +} + +function base64DecodeString (string) { + return atob(string); +} + +self.addEventListener('fetch', (event) => { + if (!event.request.url.includes('/file-decrypt/')) { + return; + } + + const url = new URL(event.request.url); + const k = base64DecodeString(url.searchParams.get('key')); + + console.log(url); + const { + key, + iv + } = JSON.parse(k); + + const newUrl = url.href.replace('/file-decrypt/', '/'); + + const requestToFetch = new Request(newUrl, event.request); + + event.respondWith( + caches.match(requestToFetch).then((response) => { + if (response) { + console.log('cached'); + return response; + } + + return fetch(requestToFetch) + .then(async (response) => { + const file = await response.arrayBuffer(); + const ivArray = base64Decode(iv); + const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); + const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); + return new Response(result); + }) + .catch((error) => { + console.error("Fetching failed:", error); + + throw error; + }); + }), + ); +}); From 640ed9ed45c6426869ccc7f60d63362c24e9784a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 27 Apr 2024 17:15:00 -0300 Subject: [PATCH 003/112] Start changing the upload to two calls --- apps/meteor/app/api/server/v1/rooms.ts | 84 +++++++++++++++++++ .../server/methods/sendFileMessage.ts | 2 +- apps/meteor/client/lib/chats/uploads.ts | 20 +++-- .../server/models/raw/BaseUploadModel.ts | 2 + packages/rest-typings/src/v1/rooms.ts | 19 +++++ 5 files changed, 120 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 9576a79f6678..c4150419f76a 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -6,6 +6,7 @@ import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps } import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; +import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/methods/eraseRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; @@ -139,6 +140,7 @@ API.v1.addRoute( }, ); +// TODO: deprecate API API.v1.addRoute( 'rooms.upload/:rid', { authRequired: true }, @@ -194,6 +196,88 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.media/:rid', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await getUploadFormData( + { + request: this.request, + }, + { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, + ); + + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; + + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + }; + + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } + + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + + const fileUrl = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + return API.v1.success({ + file: { + _id: uploadedFile._id, + url: fileUrl, + }, + }); + }, + }, +); + +API.v1.addRoute( + 'rooms.mediaConfirm/:rid/:fileId', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { + throw new Meteor.Error('invalid-file'); + } + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + + await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + + return API.v1.success({ + message, + }); + }, + }, +); + API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 6234090371f6..4f7a613ae209 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -49,7 +49,7 @@ export const parseFileIntoMessageAttachments = async ( e2e = JSON.parse(msgData.e2e); } - if (e2e.type) { + if (e2e?.type) { file.type = e2e.type; } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index a3425aa29b20..14340e535677 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -61,14 +61,9 @@ const send = async ( try { await new Promise((resolve, reject) => { const xhr = sdk.rest.upload( - `/v1/rooms.upload/${rid}`, + `/v1/rooms.media/${rid}`, { - msg, - tmid, file, - description, - t, - e2e, }, { load: (event) => { @@ -115,6 +110,19 @@ const send = async ( }, ); + xhr.onload = async () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { + msg, + tmid, + description, + t, + e2e, + }); + } + }; + if (uploads.length) { UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); } diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index 98c12965b9ff..eac646e44388 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -34,6 +34,8 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.insertOne(fileData); } + // TODO: upload as temporary, create a cron to delete non used ones and a way to mark as used + updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { if (!fileId) { return; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index b6a3f7be6b4b..733baa710c51 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -600,6 +600,25 @@ export type RoomsEndpoints = { }) => { message: IMessage | null }; }; + '/v1/rooms.media/:rid': { + POST: (params: { file: File }) => { file: { url: string } }; + }; + + '/v1/rooms.mediaConfirm/:rid/:fileId': { + POST: (params: { + description?: string; + avatar?: string; + emoji?: string; + alias?: string; + groupable?: boolean; + msg?: string; + tmid?: string; + customFields?: string; + t?: IMessage['t']; + e2e?: IMessage['e2e']; + }) => { message: IMessage | null }; + }; + '/v1/rooms.saveNotification': { POST: (params: { roomId: string; notifications: Notifications }) => void; }; From 59d99a6686ef35c187e0d069d4ec68f092bab7bb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 27 Apr 2024 21:40:46 -0300 Subject: [PATCH 004/112] Start using content property in place of e2e --- .../file-upload/server/methods/sendFileMessage.ts | 13 ++++++------- .../message/variants/room/RoomMessageContent.tsx | 6 ++++-- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 14 +++++++++----- apps/meteor/client/lib/chats/uploads.ts | 13 +++++++++++-- packages/core-typings/src/IMessage/IMessage.ts | 2 ++ packages/rest-typings/src/v1/rooms.ts | 4 ++++ 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 4f7a613ae209..f14bc07804da 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -43,14 +43,12 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; - let e2e; - console.log(msgData?.e2e); - if (msgData?.e2e) { - e2e = JSON.parse(msgData.e2e); - } + if (msgData?.content) { + const content = JSON.parse(msgData.content); - if (e2e?.type) { - file.type = e2e.type; + if (content?.file.type) { + file.type = content?.file.type; + } } const files = [ @@ -199,6 +197,7 @@ export const sendFileMessage = async ( customFields: Match.Optional(String), t: Match.Optional(String), e2e: Match.Optional(String), + content: Match.Optional(String), }), ); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 5bc16ddfdf43..ad9ac8aa73f2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -49,12 +49,14 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.e2e) { + if (!normalizedMessage.content) { return; } console.log(attachment); - const key = Base64.encode(normalizedMessage.e2e); + const content = JSON.parse(normalizedMessage.content); + + const key = Base64.encode(JSON.stringify(content.file)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 7ee613eb23f1..6da707cb8c15 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -31,7 +31,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - const uploadFile = (file: File, description?: string, extraData?: Pick) => { + const uploadFile = (file: File, description?: string, extraData?: Pick) => { chat.uploads.send(file, { description, msg, @@ -88,10 +88,14 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi if (encryptedFile) { uploadFile(encryptedFile.file, encryptedDescription, { t: 'e2e', - e2e: JSON.stringify({ - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, + // TODO: Encrypt content + content: JSON.stringify({ + file: { + // url + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }, }), }); } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 14340e535677..2ccd110d81b3 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -38,6 +38,7 @@ const send = async ( tmid, t, e2e, + content, }: { description?: string; msg?: string; @@ -45,6 +46,7 @@ const send = async ( tmid?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; + content?: string; }, ): Promise => { const id = Random.id(); @@ -119,6 +121,7 @@ const send = async ( description, t, e2e, + content, }); } }; @@ -162,6 +165,12 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e }), + { + description, + msg, + t, + e2e, + content, + }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; content?: string }, + ): Promise => send(file, { description, msg, rid, tmid, t, e2e, content }), }); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index fc73ede6ece4..a4470a3c1924 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,6 +223,8 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; + + content?: string; } export type MessageSystem = { diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 733baa710c51..176ebe40217b 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -616,6 +616,10 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; + content?: string; + encryption?: { + version: string; + }; }) => { message: IMessage | null }; }; From d84ce58afbc31c37d59897cf4be9bbf03f11d313 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 10:15:09 -0300 Subject: [PATCH 005/112] Readd e2e property --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 2 +- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index aafb2c8b99c7..9ba79ea674c1 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -440,7 +440,7 @@ export class E2ERoom extends Emitter { return { ...message, msg: data.text, - // e2e: 'done', + e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 57944ddd0297..472e71959933 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,7 @@ class E2E extends Emitter { ...message, ...(data && { msg: data.text, - // e2e: 'done', + e2e: 'done', }), }; @@ -474,7 +474,7 @@ class E2E extends Emitter { return { ...message, attachments: decryptedAttachments, - // e2e: 'done', + e2e: 'done', }; } From e6a4330582839f89aa4c8cf4986db8675da413ec Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 11:01:41 -0300 Subject: [PATCH 006/112] Client side attachment generation --- .../server/methods/sendFileMessage.ts | 74 +++++++--------- .../variants/room/RoomMessageContent.tsx | 13 ++- .../client/lib/chats/flows/uploadFiles.ts | 85 ++++++++++++++++--- apps/meteor/client/lib/chats/uploads.ts | 19 ++--- 4 files changed, 124 insertions(+), 67 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index f14bc07804da..9f74b099fa95 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -43,14 +43,6 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; - if (msgData?.content) { - const content = JSON.parse(msgData.content); - - if (content?.file.type) { - file.type = content?.file.type; - } - } - const files = [ { _id: file._id, @@ -77,39 +69,39 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // } - // } catch (e) { - // SystemLogger.error(e); - // } + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + } + } catch (e) { + SystemLogger.error(e); + } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index ad9ac8aa73f2..4d932a2580a5 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -47,16 +47,21 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + if (normalizedMessage.content) { + // TODO: decrypt + const content = JSON.parse(normalizedMessage.content); + normalizedMessage.attachments = content.attachments; + normalizedMessage.fileInfo = content.file; + } + if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content) { + if (!normalizedMessage.fileInfo) { return; } console.log(attachment); - const content = JSON.parse(normalizedMessage.content); - - const key = Base64.encode(JSON.stringify(content.file)); + const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 6da707cb8c15..1b451e465bf7 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; @@ -31,12 +31,21 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - const uploadFile = (file: File, description?: string, extraData?: Pick) => { - chat.uploads.send(file, { - description, - msg, - ...extraData, - }); + const uploadFile = ( + file: File, + description?: string, + extraData?: Pick, + getContent?: (fileId: string, fileUrl: string) => Promise, + ) => { + chat.uploads.send( + file, + { + description, + msg, + ...extraData, + }, + getContent, + ); chat.composer?.clear(); imperativeModal.close(); uploadNextFile(); @@ -86,18 +95,70 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - uploadFile(encryptedFile.file, encryptedDescription, { - t: 'e2e', + const getContent = async (_id: string, fileUrl: string) => { + const attachments = []; + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + }; + + if (/^image\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + }); + + // if (file.identify?.size) { + // attachment.image_dimensions = file.identify.size; + // } + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + // format: getFileExtension(file.name), + }); + } + // TODO: Encrypt content - content: JSON.stringify({ + return JSON.stringify({ file: { // url key: encryptedFile.key, iv: encryptedFile.iv, type: file.type, }, - }), - }); + attachments, + }); + }; + + uploadFile( + encryptedFile.file, + encryptedDescription, + { + t: 'e2e', + }, + getContent, + ); } }, invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 2ccd110d81b3..9e621d853aec 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -38,7 +38,6 @@ const send = async ( tmid, t, e2e, - content, }: { description?: string; msg?: string; @@ -46,8 +45,8 @@ const send = async ( tmid?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; - content?: string; }, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { const id = Random.id(); @@ -115,6 +114,11 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); + let content; + if (getContent) { + content = await getContent(result.file._id, result.file.url); + } + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { msg, tmid, @@ -165,12 +169,7 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { - description, - msg, - t, - e2e, - content, - }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; content?: string }, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e, content }), + { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, + ): Promise => send(file, { description, msg, rid, tmid, t, e2e }, getContent), }); From 677f0ef34b4daf52ebeaf5da33aee9a0f2795ccd Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 11:03:02 -0300 Subject: [PATCH 007/112] Cleanup --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9f74b099fa95..16d4f93c6b46 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -33,7 +33,6 @@ export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, user: IUser, - msgData?: Record, ): Promise => { validateFileRequiredFields(file); @@ -193,7 +192,7 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user, msgData); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); const msg = await executeSendMessage(userId, { rid: roomId, From 7c7eb29ae28ddd974bf965b714179b6152e54bf7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 12:44:45 -0300 Subject: [PATCH 008/112] Encrypt content --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 7 ++++++ .../variants/room/RoomMessageContent.tsx | 8 ------- .../client/lib/chats/flows/uploadFiles.ts | 24 +++++++++++-------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 472e71959933..53cdf16c6136 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -422,6 +422,13 @@ class E2E extends Emitter { const data = await e2eRoom.decrypt(message.msg); + if (message.content) { + const content = await e2eRoom.decrypt(message.content); + message.content = content; + message.attachments = content.attachments; + message.fileInfo = content.file; + } + const decryptedMessage: IE2EEMessage = { ...message, ...(data && { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4d932a2580a5..417fbd303e1c 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -47,19 +47,11 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (normalizedMessage.content) { - // TODO: decrypt - const content = JSON.parse(normalizedMessage.content); - normalizedMessage.attachments = content.attachments; - normalizedMessage.fileInfo = content.file; - } - if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { if (!normalizedMessage.fileInfo) { return; } - console.log(attachment); const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 1b451e465bf7..305a1c24fbe1 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,7 @@ import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; +import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; @@ -139,16 +140,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - // TODO: Encrypt content - return JSON.stringify({ - file: { - // url - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, - }, - attachments, - }); + const data = new TextEncoder('UTF-8').encode( + EJSON.stringify({ + file: { + // url + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }, + attachments, + }), + ); + + return e2eRoom.encryptText(data); }; uploadFile( From a9add79e4aa02315e7a2d589ba17e4d85c139dc2 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 12:52:50 -0300 Subject: [PATCH 009/112] Fix file description --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 45 +------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 53cdf16c6136..bee9549f210f 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -439,50 +439,7 @@ class E2E extends Emitter { const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); - const decryptedMessageWithAttachments = await this.decryptMessageAttachments(decryptedMessageWithQuote); - - return decryptedMessageWithAttachments; - } - - async decryptMessageAttachments(message: IMessage): Promise { - const { attachments } = message; - - if (!attachments || !attachments.length) { - return message; - } - - const e2eRoom = await this.getInstanceByRoomId(message.rid); - - if (!e2eRoom) { - return message; - } - - const decryptedAttachments = await Promise.all( - attachments.map(async (attachment) => { - if (!isFileAttachment(attachment)) { - return attachment; - } - - if (!attachment.description) { - return attachment; - } - - const data = await e2eRoom.decrypt(attachment.description); - - if (!data) { - return attachment; - } - - attachment.description = data.text; - return attachment; - }), - ); - - return { - ...message, - attachments: decryptedAttachments, - e2e: 'done', - }; + return decryptedMessageWithQuote; } async decryptPendingMessages(): Promise { From 69f815e3b671a1af2f4e18d2913de2147a96f1e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:23:51 -0300 Subject: [PATCH 010/112] Small improvements --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 1 - .../message/variants/room/RoomMessageContent.tsx | 4 ++-- apps/meteor/client/lib/chats/ChatAPI.ts | 1 + apps/meteor/client/lib/chats/flows/uploadFiles.ts | 11 +++-------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index bee9549f210f..dfe7e0821f11 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,6 @@ class E2E extends Emitter { const content = await e2eRoom.decrypt(message.content); message.content = content; message.attachments = content.attachments; - message.fileInfo = content.file; } const decryptedMessage: IE2EEMessage = { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 417fbd303e1c..b77d632a5383 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -49,11 +49,11 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.fileInfo) { + if (!normalizedMessage.content) { return; } - const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); + const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 1b466e396c21..bcc59432f376 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -103,6 +103,7 @@ export type UploadsAPI = { send( file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 305a1c24fbe1..a7ecc70579d7 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,14 +34,12 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - description?: string, - extraData?: Pick, + extraData?: Pick & { description?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, ) => { chat.uploads.send( file, { - description, msg, ...extraData, }, @@ -80,19 +78,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, description); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, description); + uploadFile(file, { description }); return; } - const encryptedDescription = await e2eRoom.encryptAttachmentDescription(description, Random.id()); - const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { @@ -157,7 +153,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi uploadFile( encryptedFile.file, - encryptedDescription, { t: 'e2e', }, From ebbe64f700e75c8492923f399f2d59a870a70348 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:29:17 -0300 Subject: [PATCH 011/112] Cleanup --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 14 -------------- .../file-upload/server/methods/sendFileMessage.ts | 1 - apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/client/lib/chats/uploads.ts | 7 ++----- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 9ba79ea674c1..37f4c3e675a6 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -410,20 +410,6 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - encryptAttachmentDescription(description, _id) { - const ts = new Date(); - - const data = new TextEncoder('UTF-8').encode( - EJSON.stringify({ - userId: this.userId, - text: description, - _id, - ts, - }), - ); - return this.encryptText(data); - } - // Decrypt messages async decryptMessage(message) { diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 16d4f93c6b46..ac36953fccf7 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,7 +187,6 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - e2e: Match.Optional(String), content: Match.Optional(String), }), ); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index a7ecc70579d7..87d74a49e04b 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,5 @@ import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 9e621d853aec..b4f632a2ff40 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -37,14 +37,12 @@ const send = async ( rid, tmid, t, - e2e, }: { description?: string; msg?: string; rid: string; tmid?: string; t?: IMessage['t']; - e2e?: IMessage['e2e']; }, getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { @@ -124,7 +122,6 @@ const send = async ( tmid, description, t, - e2e, content, }); } @@ -169,7 +166,7 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string, fileUrl: string) => Promise, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e }, getContent), + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), }); From b43f57ba029b4b3d0365c16fae7756e552529231 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:31:30 -0300 Subject: [PATCH 012/112] Cleanup --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index dfe7e0821f11..0e2c44b36ee5 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -2,7 +2,7 @@ import QueryString from 'querystring'; import URL from 'url'; import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isFileAttachment } from '@rocket.chat/core-typings'; +import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; From ec06eb3a422aef67ac0bfde8ad3cf0f1592d19a9 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:43:24 -0300 Subject: [PATCH 013/112] Improve TS --- .../variants/room/RoomMessageContent.tsx | 34 +++++++++++++------ .../core-typings/src/IMessage/IMessage.ts | 10 +++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index b77d632a5383..d215b38dbb15 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,6 +1,14 @@ import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; -import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; +import { + isDiscussionMessage, + isThreadMainMessage, + isE2EEMessage, + isFileImageAttachment, + isFileAttachment, + isFileAudioAttachment, + isFileVideoAttachment, +} from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; @@ -49,19 +57,25 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content) { + if (!normalizedMessage.content || typeof normalizedMessage.content === 'string') { return; } const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); - if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { - attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; - } - if (attachment.image_url && !attachment.image_url.startsWith('/file-decrypt/')) { - attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; - } - if (attachment.audio_url && !attachment.audio_url.startsWith('/file-decrypt/')) { - attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + + if (isFileAttachment(attachment)) { + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { + attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; + } } }); } diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index a4470a3c1924..ee6635490139 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -224,7 +224,15 @@ export interface IMessage extends IRocketChatRecord { customFields?: IMessageCustomFields; - content?: string; + content?: + | string + | { + attachments?: MessageAttachment[]; + file: { + iv: string; + key: JsonWebKey; + }; + }; } export type MessageSystem = { From f70d2e1fe006da9146746e1a2017df5215e7dc25 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 10:58:03 -0300 Subject: [PATCH 014/112] Change content data structure --- .vscode/settings.json | 1 + .../app/e2e/client/rocketchat.e2e.room.js | 22 ++++++++---------- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 9 ++++---- .../server/methods/sendFileMessage.ts | 5 +++- .../variants/room/RoomMessageContent.tsx | 9 ++++++-- apps/meteor/client/lib/chats/ChatAPI.ts | 4 ++-- .../client/lib/chats/flows/uploadFiles.ts | 23 +++++++++---------- apps/meteor/client/lib/chats/uploads.ts | 6 ++--- .../core-typings/src/IMessage/IMessage.ts | 14 ++++------- .../MessageAttachmentBase.ts | 2 ++ 10 files changed, 47 insertions(+), 48 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4eaf1836d1fd..2dcd055310d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.words": [ "autotranslate", + "ciphertext", "Contextualbar", "fname", "Gazzodown", diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 37f4c3e675a6..922af9b06521 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -375,27 +375,23 @@ export class E2ERoom extends Emitter { // Encrypts messages async encryptText(data) { - if (!(typeof data === 'function' || (typeof data === 'object' && !!data))) { - data = new TextEncoder('UTF-8').encode(EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) })); - } - - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return data; - } - const vector = crypto.getRandomValues(new Uint8Array(16)); - let result; + try { - result = await encryptAES(vector, this.groupSessionKey, data); + const result = await encryptAES(vector, this.groupSessionKey, data); + return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } catch (error) { - return this.error('Error encrypting message: ', error); + this.error('Error encrypting message: ', error); + throw error; } - - return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } // Helper function for encryption of messages encrypt(message) { + if (!this.isSupportedRoomType(this.typeOfRoom)) { + return; + } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 0e2c44b36ee5..146dbf717679 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -422,17 +422,16 @@ class E2E extends Emitter { const data = await e2eRoom.decrypt(message.msg); - if (message.content) { - const content = await e2eRoom.decrypt(message.content); - message.content = content; - message.attachments = content.attachments; + if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { + const content = await e2eRoom.decrypt(message.content.ciphertext); + Object.assign(message, content); } const decryptedMessage: IE2EEMessage = { ...message, + e2e: 'done', ...(data && { msg: data.text, - e2e: 'done', }), }; diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index ac36953fccf7..9f6aedbf7e3a 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,7 +187,10 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - content: Match.Optional(String), + content: Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), }), ); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index d215b38dbb15..a56105384e44 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -57,11 +57,16 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content || typeof normalizedMessage.content === 'string') { + if (!encrypted || message.e2e !== 'done') { return; } - const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); + const key = Base64.encode( + JSON.stringify({ + key: attachment.key, + iv: attachment.iv, + }), + ); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index bcc59432f376..b903528f1d34 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IE2EEMessage } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; @@ -103,7 +103,7 @@ export type UploadsAPI = { send( file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 87d74a49e04b..45503ba5d994 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,4 +1,4 @@ -import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { EJSON } from 'meteor/ejson'; @@ -33,8 +33,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { description?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string, fileUrl: string) => Promise, ) => { chat.uploads.send( file, @@ -91,7 +91,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string) => { + const getContent = async (_id: string, fileUrl: string): Promise => { const attachments = []; const attachment: FileAttachmentProps = { @@ -100,6 +100,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi description, title_link: fileUrl, title_link_download: true, + key: encryptedFile.key, + iv: encryptedFile.iv, }; if (/^image\/.+/.test(file.type)) { @@ -135,19 +137,16 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - const data = new TextEncoder('UTF-8').encode( + const data = new TextEncoder().encode( EJSON.stringify({ - file: { - // url - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, - }, attachments, }), ); - return e2eRoom.encryptText(data); + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await e2eRoom.encryptText(data), + }; }; uploadFile( diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index b4f632a2ff40..14c695efd3c0 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; @@ -44,7 +44,7 @@ const send = async ( tmid?: string; t?: IMessage['t']; }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { const id = Random.id(); @@ -167,6 +167,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes send: ( file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), }); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index ee6635490139..87293f830c42 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,16 +223,6 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; - - content?: - | string - | { - attachments?: MessageAttachment[]; - file: { - iv: string; - key: JsonWebKey; - }; - }; } export type MessageSystem = { @@ -367,6 +357,10 @@ export const isVoipMessage = (message: IMessage): message is IVoipMessage => 'vo export type IE2EEMessage = IMessage & { t: 'e2e'; e2e: 'pending' | 'done'; + content?: { + algorithm: 'rc.v1.aes-sha2'; + ciphertext: string; // Encrypted subset JSON of IMessage + }; }; export interface IOTRMessage extends IMessage { diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 167975ae014f..1f6fff1444c9 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -13,4 +13,6 @@ export type MessageAttachmentBase = { format?: string; title_link?: string; title_link_download?: boolean; + iv?: string; + key?: JsonWebKey; }; From 445c5283d5950e3d698eada3a4999e63802c6847 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 11:37:02 -0300 Subject: [PATCH 015/112] Move attachment key info to inside encryption property --- .../message/variants/room/RoomMessageContent.tsx | 11 +++-------- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 6 ++++-- .../MessageAttachment/MessageAttachmentBase.ts | 6 ++++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index a56105384e44..4f90b0b8f889 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -55,18 +55,13 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (normalizedMessage?.attachments?.length) { + if (encrypted && normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!encrypted || message.e2e !== 'done') { + if (!attachment.encryption) { return; } - const key = Base64.encode( - JSON.stringify({ - key: attachment.key, - iv: attachment.iv, - }), - ); + const key = Base64.encode(JSON.stringify(attachment.encryption)); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 45503ba5d994..bb29fa62d040 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -100,8 +100,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi description, title_link: fileUrl, title_link_download: true, - key: encryptedFile.key, - iv: encryptedFile.iv, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, }; if (/^image\/.+/.test(file.type)) { diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 1f6fff1444c9..23a999844145 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -13,6 +13,8 @@ export type MessageAttachmentBase = { format?: string; title_link?: string; title_link_download?: boolean; - iv?: string; - key?: JsonWebKey; + encryption?: { + iv: string; + key: JsonWebKey; + }; }; From 6daab1a96bfc05d0a5a812cd1a792ea76af3ebca Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 14:21:09 -0300 Subject: [PATCH 016/112] Set encrypted file name as a hash sha-256 of the name --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 922af9b06521..862e28cb3fe8 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -335,6 +335,13 @@ export class E2ERoom extends Emitter { } } + async sha256Hash(text) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data))); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } + // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { // if (!this.isSupportedRoomType(this.typeOfRoom)) { @@ -355,7 +362,9 @@ export class E2ERoom extends Emitter { const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const encryptedFile = new File([toArrayBuffer(result)], file.name); + const fileName = await this.sha256Hash(file.name); + + const encryptedFile = new File([toArrayBuffer(result)], fileName); return { file: encryptedFile, From fd5fcd29cee66d7a9994a07039bdeb3a4a5185fa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 14:31:16 -0300 Subject: [PATCH 017/112] Fix lint and TS --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 5 ----- apps/meteor/client/startup/e2e.ts | 4 +++- packages/rest-typings/src/v1/rooms.ts | 8 ++------ 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 862e28cb3fe8..215227781e37 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -1,6 +1,5 @@ import { Base64 } from '@rocket.chat/base64'; import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { RoomManager } from '../../../client/lib/RoomManager'; @@ -436,10 +435,6 @@ export class E2ERoom extends Emitter { } async decrypt(message) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return message; - } - const keyID = message.slice(0, 12); if (keyID !== this.keyID) { diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index f9cf156f8d8b..bda1d5e9a0b6 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -142,7 +142,9 @@ Meteor.startup(() => { // Should encrypt this message. const msg = await e2eRoom.encrypt(message); - message.msg = msg; + if (msg) { + message.msg = msg; + } message.t = 'e2e'; message.e2e = 'pending'; return message; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 176ebe40217b..36d8aa87a921 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, IUser, RoomAdminFieldsType, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser, RoomAdminFieldsType, IUpload, IE2EEMessage } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; @@ -615,11 +615,7 @@ export type RoomsEndpoints = { tmid?: string; customFields?: string; t?: IMessage['t']; - e2e?: IMessage['e2e']; - content?: string; - encryption?: { - version: string; - }; + content?: IE2EEMessage['content']; }) => { message: IMessage | null }; }; From 2e1f78a7baa1778f0b89070fee5665f6422eac89 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 15:50:53 -0300 Subject: [PATCH 018/112] Fix regression --- .../app/file-upload/server/methods/sendFileMessage.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9f6aedbf7e3a..19579d7db9c6 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,10 +187,12 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - content: Match.ObjectIncluding({ - algorithm: String, - ciphertext: String, - }), + content: Match.Optional( + Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), + ), }), ); From 1e25638ef9cac74cb5e023e6b724b1497ba0ed9b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 9 May 2024 10:04:45 -0300 Subject: [PATCH 019/112] Mark uploads as temporary and confirm on send message --- apps/meteor/app/api/server/v1/rooms.ts | 6 +++++ .../server/models/raw/BaseUploadModel.ts | 22 +++++++++++++++++-- .../src/models/IBaseUploadsModel.ts | 2 ++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index c4150419f76a..6f9eed8b02f9 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -218,12 +218,16 @@ API.v1.addRoute( let { fileBuffer } = file; + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + const details = { name: file.filename, size: fileBuffer.length, type: file.mimetype, rid: this.urlParams.rid, userId: this.userId, + expiresAt, }; const stripExif = settings.get('Message_Attachments_Strip_Exif'); @@ -267,6 +271,8 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index eac646e44388..91468e14d6e9 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -11,6 +11,7 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return [ { key: { name: 1 }, sparse: true }, { key: { rid: 1 }, sparse: true }, + { key: { expiresAt: 1 }, sparse: true }, ]; } @@ -34,8 +35,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.insertOne(fileData); } - // TODO: upload as temporary, create a cron to delete non used ones and a way to mark as used - updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { if (!fileId) { return; @@ -63,6 +62,25 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } + confirmTemporaryFile(fileId: string, userId: string): Promise | undefined { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId, + }; + + const update: Filter = { + $unset: { + expiresAt: 1, + }, + }; + + return this.updateOne(filter, update); + } + async findOneByName(name: string): Promise { return this.findOne({ name }); } diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 8995b78eda08..940a5cf657a4 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -8,6 +8,8 @@ export interface IBaseUploadsModel extends IBaseModel { updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined; + confirmTemporaryFile(fileId: string, userId: string): Promise | undefined; + findOneByName(name: string): Promise; findOneByRoomId(rid: string): Promise; From 574169f58ca8d0f7cc18634ac0830e66e75ea58e Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 9 May 2024 10:21:01 -0300 Subject: [PATCH 020/112] Fix API test --- apps/meteor/tests/end-to-end/api/09-rooms.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 4cd3806c9e86..d5a24474db74 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -305,7 +305,6 @@ describe('[Rooms]', function () { .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .field('t', 'e2e') - .field('e2e', 'pending') .field('description', 'some_file_description') .attach('file', imgURL) .expect('Content-Type', 'application/json') @@ -323,7 +322,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); expect(res.body.message).to.have.property('t', 'e2e'); - expect(res.body.message).to.have.property('e2e', 'pending'); }); }); }); From 7154756f2d4eaee25f7aac819ac32312673c503a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 13:52:06 -0300 Subject: [PATCH 021/112] Implement cronjob --- apps/meteor/app/api/server/v1/rooms.ts | 4 ++-- .../server/cron/temporaryUploadsCleanup.ts | 16 +++++++++++++ .../server/models/raw/BaseUploadModel.ts | 24 ++++++++++++++++++- apps/meteor/server/startup/cron.ts | 2 ++ .../src/models/IBaseUploadsModel.ts | 4 +++- 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/server/cron/temporaryUploadsCleanup.ts diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 6f9eed8b02f9..a7b07c753470 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -271,10 +271,10 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); - await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); return API.v1.success({ diff --git a/apps/meteor/server/cron/temporaryUploadsCleanup.ts b/apps/meteor/server/cron/temporaryUploadsCleanup.ts new file mode 100644 index 000000000000..b227dd31947f --- /dev/null +++ b/apps/meteor/server/cron/temporaryUploadsCleanup.ts @@ -0,0 +1,16 @@ +import { cronJobs } from '@rocket.chat/cron'; +import { Uploads } from '@rocket.chat/models'; + +import { FileUpload } from '../../app/file-upload/server'; + +async function temporaryUploadCleanup(): Promise { + const files = await Uploads.findExpiredTemporaryFiles({ projection: { _id: 1 } }).toArray(); + + for await (const file of files) { + await FileUpload.getStore('Uploads').deleteById(file._id); + } +} + +export async function temporaryUploadCleanupCron(): Promise { + await cronJobs.add('temporaryUploadCleanup', '31 * * * *', async () => temporaryUploadCleanup()); +} diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index 91468e14d6e9..d0e187a91b23 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -1,6 +1,16 @@ import type { IUpload } from '@rocket.chat/core-typings'; import type { IBaseUploadsModel } from '@rocket.chat/model-typings'; -import type { DeleteResult, IndexDescription, UpdateResult, Document, InsertOneResult, WithId, Filter } from 'mongodb'; +import type { + DeleteResult, + IndexDescription, + UpdateResult, + Document, + InsertOneResult, + WithId, + Filter, + FindOptions, + FindCursor, +} from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -78,6 +88,7 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo }, }; + console.log('confirm', filter, update); return this.updateOne(filter, update); } @@ -89,6 +100,17 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.findOne({ rid }); } + findExpiredTemporaryFiles(options?: FindOptions): FindCursor { + return this.find( + { + expiresAt: { + $lte: new Date(), + }, + }, + options, + ); + } + async updateFileNameById(fileId: string, name: string): Promise { const filter = { _id: fileId }; const update = { diff --git a/apps/meteor/server/startup/cron.ts b/apps/meteor/server/startup/cron.ts index 3c0482069e79..6057186a1e4e 100644 --- a/apps/meteor/server/startup/cron.ts +++ b/apps/meteor/server/startup/cron.ts @@ -6,6 +6,7 @@ import { npsCron } from '../cron/nps'; import { oembedCron } from '../cron/oembed'; import { startCron } from '../cron/start'; import { statsCron } from '../cron/statistics'; +import { temporaryUploadCleanupCron } from '../cron/temporaryUploadsCleanup'; import { userDataDownloadsCron } from '../cron/userDataDownloads'; import { videoConferencesCron } from '../cron/videoConferences'; @@ -17,6 +18,7 @@ Meteor.defer(async () => { await oembedCron(); await statsCron(logger); await npsCron(); + await temporaryUploadCleanupCron(); await federationCron(); await videoConferencesCron(); await userDataDownloadsCron(); diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 940a5cf657a4..12db0ee25d6b 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -1,5 +1,5 @@ import type { IUpload } from '@rocket.chat/core-typings'; -import type { DeleteResult, UpdateResult, Document, InsertOneResult, WithId } from 'mongodb'; +import type { DeleteResult, UpdateResult, Document, InsertOneResult, WithId, FindCursor, FindOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -14,6 +14,8 @@ export interface IBaseUploadsModel extends IBaseModel { findOneByRoomId(rid: string): Promise; + findExpiredTemporaryFiles(options?: FindOptions): FindCursor; + updateFileNameById(fileId: string, name: string): Promise; deleteFile(fileId: string): Promise; From aff80f33581e177bbf9fc21ae1596ce771d2c2ea Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 16:34:36 -0300 Subject: [PATCH 022/112] Skip UI attachment test for now --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index b63efa2d092f..26ad3b9721e6 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -311,7 +311,8 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); - test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { + // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted + test.skip('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); From de5f0f23127f54230a610d798ecc1d4d90207e91 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 17:14:10 -0300 Subject: [PATCH 023/112] Do not generate attachment on backend for e2ee messages --- .../server/methods/sendFileMessage.ts | 18 +++++++++++------- .../variants/room/RoomMessageContent.tsx | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 19579d7db9c6..9393d14fc120 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -196,19 +196,23 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - - const msg = await executeSendMessage(userId, { + const data = { rid: roomId, ts: new Date(), - file: files[0], - files, - attachments, ...(msgData as Partial), ...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }), msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, - }); + }; + + if (msgData?.t !== 'e2e') { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + data.file = files[0]; + data.files = files; + data.attachments = attachments; + } + + const msg = await executeSendMessage(userId, data); callbacks.runAsync('afterFileUpload', { user, room, message: msg }); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4f90b0b8f889..5a78ce5a7cbc 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -82,6 +82,8 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( @@ -93,7 +95,6 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM searchText={searchText} /> )} - {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} )} From 939940d90687274d3e32d7c6917c8a21df4ce2d2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 14 May 2024 11:08:41 -0300 Subject: [PATCH 024/112] Download through serviceworker --- .../structure/AttachmentDownload.tsx | 25 ++-- .../structure/AttachmentDownloadBase.tsx | 26 ++++ .../structure/AttachmentEncryptedDownload.tsx | 15 ++ .../attachments/structure/AttachmentImage.tsx | 1 + .../message/hooks/useNormalizedMessage.ts | 30 +++- .../variants/room/RoomMessageContent.tsx | 36 +---- .../hooks/useDownloadFromServiceWorker.ts | 49 +++++++ apps/meteor/public/enc.js | 131 ++++++++++++------ apps/meteor/server/routes/fileDecrypt.ts | 7 + apps/meteor/server/routes/index.ts | 1 + 10 files changed, 227 insertions(+), 94 deletions(-) create mode 100644 apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx create mode 100644 apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx create mode 100644 apps/meteor/client/hooks/useDownloadFromServiceWorker.ts create mode 100644 apps/meteor/server/routes/fileDecrypt.ts diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx index de790f9e3936..b76cb268bb2f 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx @@ -1,25 +1,20 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, FC } from 'react'; import React from 'react'; -import Action from '../../Action'; +import type Action from '../../Action'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; +import AttachmentEncryptedDownload from './AttachmentEncryptedDownload'; type AttachmentDownloadProps = Omit, 'icon'> & { title?: string | undefined; href: string }; const AttachmentDownload: FC = ({ title, href, ...props }) => { - const t = useTranslation(); - return ( - - ); + const isEncrypted = href.includes('/file-decrypt/'); + + if (isEncrypted) { + return ; + } + + return ; }; export default AttachmentDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx new file mode 100644 index 000000000000..48c078b9146c --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import Action from '../../Action'; + +type AttachmentDownloadBaseProps = Omit, 'icon'> & { title?: string | undefined; href: string }; + +const AttachmentDownloadBase: FC = ({ title, href, ...props }) => { + const t = useTranslation(); + + return ( + + ); +}; + +export default AttachmentDownloadBase; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx new file mode 100644 index 000000000000..1dc6752abdd0 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx @@ -0,0 +1,15 @@ +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; + +type AttachmentDownloadProps = ComponentProps; + +const AttachmentEncryptedDownload: FC = ({ title, href, ...props }) => { + const encryptedAnchorProps = useDownloadFromServiceWorker(href, title); + + return ; +}; + +export default AttachmentEncryptedDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 5b251ad6143f..8195fdee5973 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -86,6 +86,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo alt='' width={dimensions.width} height={dimensions.height} + loading='lazy' /> diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 2ade3deb4926..6898c55ebad2 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -1,3 +1,5 @@ +import { Base64 } from '@rocket.chat/base64'; +import { isFileImageAttachment, isFileAttachment, isFileAudioAttachment, isFileVideoAttachment } from '@rocket.chat/core-typings'; import type { IMessage } from '@rocket.chat/core-typings'; import type { Options } from '@rocket.chat/message-parser'; import { useSetting } from '@rocket.chat/ui-contexts'; @@ -30,6 +32,32 @@ export const useNormalizedMessage = (message: TMessag }), }; - return parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + + normalizedMessage.attachments = normalizedMessage.attachments?.map((attachment) => { + if (!attachment.encryption) { + return attachment; + } + + const key = Base64.encode(JSON.stringify(attachment.encryption)); + + if (isFileAttachment(attachment)) { + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { + attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; + } + } + return attachment; + }); + + return normalizedMessage; }, [showColors, customDomains, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, message, autoTranslateOptions]); }; diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 5a78ce5a7cbc..804002e2ffaf 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,14 +1,5 @@ -import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; -import { - isDiscussionMessage, - isThreadMainMessage, - isE2EEMessage, - isFileImageAttachment, - isFileAttachment, - isFileAudioAttachment, - isFileVideoAttachment, -} from '@rocket.chat/core-typings'; +import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; @@ -55,31 +46,6 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (encrypted && normalizedMessage?.attachments?.length) { - normalizedMessage.attachments.forEach((attachment) => { - if (!attachment.encryption) { - return; - } - - const key = Base64.encode(JSON.stringify(attachment.encryption)); - - if (isFileAttachment(attachment)) { - if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { - attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; - } - if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { - attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; - } - if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { - attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; - } - if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { - attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; - } - } - }); - } - return ( <> {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} diff --git a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts new file mode 100644 index 000000000000..9a44d8d5fd23 --- /dev/null +++ b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts @@ -0,0 +1,49 @@ +import { Emitter } from '@rocket.chat/emitter'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { downloadAs } from '../lib/download'; + +const ee = new Emitter>(); + +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; + + ee.emit(event.data.id, { result, id: event.data.id }); + } +}); + +export const useDownloadFromServiceWorker = (href: string, title?: string) => { + const { controller } = navigator.serviceWorker; + + const uid = useUniqueId(); + + const { t } = useTranslation(); + + useEffect( + () => + ee.once(uid, ({ result }) => { + downloadAs({ data: [new Blob([result])] }, title ?? t('Download')); + }), + [title, t, uid], + ); + + return { + disabled: !controller, + onContextMenu: useCallback((e) => e.preventDefault(), []), + onClick: useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + + controller?.postMessage({ + type: 'attachment-download', + url: href, + id: uid, + }); + }, + [href, uid, controller], + ), + }; +}; diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 25b74110b11a..ea296f031e8a 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -1,55 +1,100 @@ -function base64Decode (string) { +function base64Decode(string) { string = atob(string); - const - length = string.length, - buf = new ArrayBuffer(length), - bufView = new Uint8Array(buf); - for (var i = 0; i < string.length; i++) { bufView[i] = string.charCodeAt(i) } - return buf + const length = string.length, + buf = new ArrayBuffer(length), + bufView = new Uint8Array(buf); + for (var i = 0; i < string.length; i++) { + bufView[i] = string.charCodeAt(i); + } + return buf; } -function base64DecodeString (string) { +function base64DecodeString(string) { return atob(string); } +const decrypt = async (key, iv, file) => { + const ivArray = base64Decode(iv); + const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); + const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); + + return result; +}; +const getUrlParams = (url) => { + const urlObj = new URL(url); + + const k = base64DecodeString(urlObj.searchParams.get('key')); + + const { key, iv } = JSON.parse(k); + + const newUrl = urlObj.href.replace('/file-decrypt/', '/'); + + return { key, iv, url: newUrl }; +}; + self.addEventListener('fetch', (event) => { if (!event.request.url.includes('/file-decrypt/')) { return; } - const url = new URL(event.request.url); - const k = base64DecodeString(url.searchParams.get('key')); - - console.log(url); - const { - key, - iv - } = JSON.parse(k); - - const newUrl = url.href.replace('/file-decrypt/', '/'); - - const requestToFetch = new Request(newUrl, event.request); - - event.respondWith( - caches.match(requestToFetch).then((response) => { - if (response) { - console.log('cached'); - return response; - } - - return fetch(requestToFetch) - .then(async (response) => { - const file = await response.arrayBuffer(); - const ivArray = base64Decode(iv); - const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); - return new Response(result); - }) - .catch((error) => { - console.error("Fetching failed:", error); - - throw error; - }); - }), - ); + try { + const { url, key, iv } = getUrlParams(event.request.url); + + const requestToFetch = new Request(url, event.request); + + event.respondWith( + caches.match(requestToFetch).then((response) => { + if (response) { + return response; + } + + return fetch(requestToFetch) + .then(async (res) => { + const file = await res.arrayBuffer(); + const result = await decrypt(key, iv, file); + const response = new Response(result); + await caches.open('v1').then((cache) => { + cache.put(requestToFetch, response.clone()); + }); + + return response; + }) + .catch((error) => { + console.error('Fetching failed:', error); + + throw error; + }); + }), + ); + } catch (error) { + console.error(error); + throw error; + } +}); + +self.addEventListener('message', async (event) => { + if (event.data.type !== 'attachment-download') { + return; + } + const requestToFetch = new Request(url); + + const { url, key, iv } = getUrlParams(event.data.url); + const res = (await caches.match(requestToFetch)) ?? (await fetch(url)); + + const file = await res.arrayBuffer(); + const result = await decrypt(key, iv, file); + event.source + .postMessage({ + id: event.data.id, + type: 'attachment-download-result', + result, + }) + .catch((error) => { + console.error('Posting message failed:', error); + event.source.postMessage({ + id: event.data.id, + type: 'attachment-download-result', + error, + }); + }); }); diff --git a/apps/meteor/server/routes/fileDecrypt.ts b/apps/meteor/server/routes/fileDecrypt.ts new file mode 100644 index 000000000000..005c0202658f --- /dev/null +++ b/apps/meteor/server/routes/fileDecrypt.ts @@ -0,0 +1,7 @@ +import { WebApp } from 'meteor/webapp'; + +WebApp.connectHandlers.use('/file-decrypt/', (_, res) => { + // returns 404 + res.writeHead(404); + res.end('Not found'); +}); diff --git a/apps/meteor/server/routes/index.ts b/apps/meteor/server/routes/index.ts index e60f0ceb3f24..4abc6c6f044a 100644 --- a/apps/meteor/server/routes/index.ts +++ b/apps/meteor/server/routes/index.ts @@ -2,4 +2,5 @@ import './avatar'; import './health'; import './i18n'; import './timesync'; +import './fileDecrypt'; import './userDataDownload'; From 26e2eb84df605c3b9077dcc9284cc522203d78b7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 15 May 2024 10:27:51 -0300 Subject: [PATCH 025/112] =?UTF-8?q?=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index d393f7182036..bb29fa62d040 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -2,6 +2,7 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/c import { isRoomFederated } from '@rocket.chat/core-typings'; import { EJSON } from 'meteor/ejson'; +import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; import { imperativeModal } from '../../imperativeModal'; @@ -66,7 +67,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi imperativeModal.close(); uploadNextFile(); }, - onSubmit: (fileName: string, description?: string): void => { + onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, From aacf63dbea54d6582636e928f14911d755e99d8b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 12 Jun 2024 11:00:06 +0530 Subject: [PATCH 026/112] testing with e2e --- ee/packages/api-client/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index 7a664b9fcc8a..c1648e87c615 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -221,6 +221,7 @@ export class RestClient implements RestClientInterface { } send(endpoint: string, method: string, { headers, ...options }: Omit = {}): Promise { + console.log(`hello ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); return fetch(`${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, { ...options, headers: { ...this.getCredentialsAsHeaders(), ...this.headers, ...headers }, @@ -281,7 +282,7 @@ export class RestClient implements RestClientInterface { } value && data.append(key, value as any); }); - + console.log(`hello from file ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => { xhr.setRequestHeader(key, value); From ee3fe8afd6dd1720bef976fc85924deedd59867d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 12 Jun 2024 17:11:40 -0300 Subject: [PATCH 027/112] Move encryption of msg to inside content --- .../app/e2e/client/rocketchat.e2e.room.js | 39 ++++++++++++++++--- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 15 +------ apps/meteor/client/lib/chats/data.ts | 5 +-- .../client/lib/chats/flows/sendMessage.ts | 3 ++ .../client/lib/chats/flows/uploadFiles.ts | 13 ++----- apps/meteor/client/startup/e2e.ts | 13 ++----- 6 files changed, 46 insertions(+), 42 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index e1d18ddadc35..16e431ed7a97 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -403,6 +403,30 @@ export class E2ERoom extends Emitter { } } + // Helper function for encryption of content + async encryptMessageContent(contentToBeEncrypted) { + const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); + + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await this.encryptText(data), + }; + } + + // Helper function for encryption of content + async encryptMessage(message) { + const { msg, attachments, ...rest } = message; + + const content = await this.encryptMessageContent({ msg, attachments }); + + return { + ...rest, + content, + t: 'e2e', + e2e: 'pending', + }; + } + // Helper function for encryption of messages encrypt(message) { if (!this.isSupportedRoomType(this.typeOfRoom)) { @@ -424,21 +448,26 @@ export class E2ERoom extends Emitter { } // Decrypt messages - async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } - const data = await this.decrypt(message.msg); + if (message.msg) { + const data = await this.decrypt(message.msg); - if (!data?.text) { - return message; + if (data?.text) { + message.msg = data.text; + } + } + + if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(message.content.ciphertext); + Object.assign(message, content); } return { ...message, - msg: data.text, e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 89331b762b55..ec21b47d1c5b 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -521,20 +521,7 @@ class E2E extends Emitter { return message; } - const data = await e2eRoom.decrypt(message.msg); - - if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { - const content = await e2eRoom.decrypt(message.content.ciphertext); - Object.assign(message, content); - } - - const decryptedMessage: IE2EEMessage = { - ...message, - e2e: 'done', - ...(data && { - msg: data.text, - }), - }; + const decryptedMessage: IE2EEMessage = await e2eRoom.decryptMessage(message); const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index 12fb4097dce5..830ae940f91a 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -7,7 +7,6 @@ import { Messages, ChatRoom, ChatSubscription } from '../../../app/models/client import { settings } from '../../../app/settings/client'; import { MessageTypes } from '../../../app/ui-utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { onClientBeforeSendMessage } from '../onClientBeforeSendMessage'; import { prependReplies } from '../utils/prependReplies'; import type { DataAPI } from './ChatAPI'; @@ -21,7 +20,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const effectiveRID = originalMessage?.rid ?? rid; const effectiveTMID = originalMessage ? originalMessage.tmid : tmid; - return (await onClientBeforeSendMessage({ + return { _id: originalMessage?._id ?? Random.id(), rid: effectiveRID, ...(effectiveTMID && { @@ -29,7 +28,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage ...(sendToChannel && { tshow: sendToChannel }), }), msg, - })) as IMessage; + }; }; const findMessageByID = async (mid: IMessage['_id']): Promise => diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 62adbd80eb3e..0d05bce507de 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -3,6 +3,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; +import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { processMessageEditing } from './processMessageEditing'; @@ -29,6 +30,8 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]) return; } + message = (await onClientBeforeSendMessage(message)) as IMessage; + await sdk.call('sendMessage', message, previewUrls); }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index bb29fa62d040..45dc8bbdfd50 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -139,16 +139,9 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - const data = new TextEncoder().encode( - EJSON.stringify({ - attachments, - }), - ); - - return { - algorithm: 'rc.v1.aes-sha2', - ciphertext: await e2eRoom.encryptText(data), - }; + return e2eRoom.encryptMessageContent({ + attachments, + }); }; uploadFile( diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index 255fb7c3387c..21473e1585b0 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -1,4 +1,4 @@ -import type { AtLeast, IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -129,7 +129,7 @@ Meteor.startup(() => { }); // Encrypt messages before sending - offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message: AtLeast) => { + offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message) => { const e2eRoom = await e2e.getInstanceByRoomId(message.rid); if (!e2eRoom) { @@ -147,14 +147,7 @@ Meteor.startup(() => { } // Should encrypt this message. - const msg = await e2eRoom.encrypt(message); - - if (msg) { - message.msg = msg; - } - message.t = 'e2e'; - message.e2e = 'pending'; - return message; + return e2eRoom.encryptMessage(message); }); listenersAttached = true; From 48a97f741901fe0a764586ec125c12cbaa8ed5fa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:04:50 -0300 Subject: [PATCH 028/112] Fix placeholder message for threads --- .../message/variants/thread/ThreadMessageContent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 7098a2709d5b..2f341dc99a23 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -1,5 +1,6 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; +import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -37,14 +38,17 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const normalizedMessage = useNormalizedMessage(message); + const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( )} - {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} From 446b1a71a6e31b705927d57b2db6053123184737 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:06:45 -0300 Subject: [PATCH 029/112] Unskip test --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index be14273cf9b4..9ead233affa6 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -240,7 +240,7 @@ test.describe.serial('e2e-encryption', () => { }); // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted - test.skip('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { + test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); From c258b218a3bb600d6b7da6c40e202c5176be5af8 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:12:49 -0300 Subject: [PATCH 030/112] Fix ts --- apps/meteor/client/lib/chats/data.ts | 2 +- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/package.json | 2 +- packages/core-typings/src/IMessage/IMessage.ts | 9 +++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index 830ae940f91a..445f61f27226 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -28,7 +28,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage ...(sendToChannel && { tshow: sendToChannel }), }), msg, - }; + } as IMessage; }; const findMessageByID = async (mid: IMessage['_id']): Promise => diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 45dc8bbdfd50..47941e8f5f6e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,5 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index d79155167845..eae52435a2dd 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -25,7 +25,7 @@ "debug": "meteor run --inspect", "debug-brk": "meteor run --inspect-brk", "lint": "yarn stylelint && yarn eslint", - "eslint": "NODE_OPTIONS=\"--max-old-space-size=4098\" eslint --ext .js,.jsx,.ts,.tsx . --cache", + "eslint": "NODE_OPTIONS=\"--max-old-space-size=6144\" eslint --ext .js,.jsx,.ts,.tsx . --cache", "eslint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix --cache", "obj:dev": "TEST_MODE=true yarn dev", "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 87293f830c42..8730a6a0cea7 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,6 +223,11 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; + + content?: { + algorithm: string; // 'rc.v1.aes-sha2' + ciphertext: string; // Encrypted subset JSON of IMessage + }; } export type MessageSystem = { @@ -357,10 +362,6 @@ export const isVoipMessage = (message: IMessage): message is IVoipMessage => 'vo export type IE2EEMessage = IMessage & { t: 'e2e'; e2e: 'pending' | 'done'; - content?: { - algorithm: 'rc.v1.aes-sha2'; - ciphertext: string; // Encrypted subset JSON of IMessage - }; }; export interface IOTRMessage extends IMessage { From 989c61c616e9ad14a6917911c0385dbe94d7adf6 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 16:46:08 -0300 Subject: [PATCH 031/112] Fix tests --- apps/meteor/app/api/server/v1/rooms.ts | 6 +++++- .../app/file-upload/server/methods/sendFileMessage.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index d5ca92377eb4..c05557873ba8 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -271,7 +271,11 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; - await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + await sendFileMessage( + this.userId, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { parseAttachmentsForE2EE: false }, + ); await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9393d14fc120..485528a5e62f 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -159,6 +159,13 @@ export const sendFileMessage = async ( file: Partial; msgData?: Record; }, + { + parseAttachmentsForE2EE, + }: { + parseAttachmentsForE2EE: boolean; + } = { + parseAttachmentsForE2EE: true, + }, ): Promise => { const user = await Users.findOneById(userId); if (!user) { @@ -205,7 +212,7 @@ export const sendFileMessage = async ( groupable: msgData?.groupable ?? false, }; - if (msgData?.t !== 'e2e') { + if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; data.files = files; From 9da964021641d63ed8e4dcb418db24926248f39d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 14 Jun 2024 16:08:36 -0300 Subject: [PATCH 032/112] Add test for old e2ee msg format --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 45 +++++++++++++++++++ .../tests/e2e/fixtures/collections/users.ts | 3 +- .../tests/e2e/fixtures/inject-initial-data.ts | 7 ++- apps/meteor/tests/e2e/fixtures/userStates.ts | 31 +++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 6b60bd4b637a..5f42ba5a5a4e 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -517,3 +517,48 @@ test.describe.serial('e2ee room setup', () => { await expect(page.locator('.rcx-states__title')).toContainText('Check back later'); }); }); + +test.describe.serial('e2ee support legacy formats', () => { + let poHomeChannel: HomeChannel; + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + }); + + test.beforeAll(async ({ api }) => { + test.use({ storageState: Users.userE2EE.state }); + + expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + const rid = await page.locator('[data-qa-rc-room]').getAttribute('data-qa-rc-room'); + console.log({ rid }); + + // send old format encrypted message via API + await api.post('/chat.sendMessage', { + rid, + msg: 'eyJhbGciOiJB8NgMxt0P2jW/aRt4y4++lb8LDSpxUExisX1SiXaKCO9FkfrS1HfO7gFS0nxzHu3CjfgRAK5o3A9kWc4PrHfsQnTTLSh5LW/BKpgnu8XLoEoCbP3FTfy14i5urg6QnZRpz9jUsESjXXIFSxLzEx/T2PzuAzwNROhS+omrNKFi3r9oqkfzUZgTAVzitRpkJ7VN', + e2e: true, + }); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('Old format message'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/fixtures/collections/users.ts b/apps/meteor/tests/e2e/fixtures/collections/users.ts index 5b85974fb57c..5baaccc10134 100644 --- a/apps/meteor/tests/e2e/fixtures/collections/users.ts +++ b/apps/meteor/tests/e2e/fixtures/collections/users.ts @@ -10,7 +10,7 @@ type UserFixture = IUser & { }; export function createUserFixture(user: IUserState): UserFixture { - const { username, hashedToken, loginExpire } = user.data; + const { username, hashedToken, loginExpire, e2e } = user.data; return { _id: `${username}`, @@ -48,5 +48,6 @@ export function createUserFixture(user: IUserState): UserFixture { createdAt: new Date(), _updatedAt: new Date(), __rooms: ['GENERAL'], + e2e, }; } diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts index e7e68790cf3d..5a28e534cc16 100644 --- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts +++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts @@ -7,7 +7,12 @@ import { Users } from './userStates'; export default async function injectInitialData() { const connection = await MongoClient.connect(constants.URL_MONGODB); - const usersFixtures = [createUserFixture(Users.user1), createUserFixture(Users.user2), createUserFixture(Users.user3)]; + const usersFixtures = [ + createUserFixture(Users.user1), + createUserFixture(Users.user2), + createUserFixture(Users.user3), + createUserFixture(Users.userE2EE), + ]; await Promise.all( usersFixtures.map((user) => diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts index e948d6acf5a7..219e40f1ab22 100644 --- a/apps/meteor/tests/e2e/fixtures/userStates.ts +++ b/apps/meteor/tests/e2e/fixtures/userStates.ts @@ -11,10 +11,30 @@ export type IUserState = { loginToken: string; loginExpire: Date; hashedToken: string; + e2e?: { + private_key: string; + public_key: string; + }; + e2ePassword?: string; }; state: Exclude; }; +const e2eeData: Record = { + userE2EE: { + server: { + private_key: + '{"$binary":"F4AdhH8Nq2MkGy0+IpfSH5OfN6FnTh1blyhxNucQLUxskZiccAKyXyszquv7h3Jz5nFFPj5LxH18ZUTmqcMVz7LHIlOtF74Pq+g79LqWylXO2ANfxBbNUg6xhD7FFWn9eHI9k0gZy7dPmtuSiwlJzYPYZK/ouKcnwgkhze2Nz455JAf7gREvMEUc5UUCMsaQ2ka26gMU0/zUnkRuIqL0evIU6V2Y09+GmsUo4BUQvnvA657re+mP7XosK0LU6R+nO9m/cGSezCMLrzJ8TXxpCWIwgfzedR6XxE6Xb7qDpf2dNoUXEQsgUmp0Ft0/3BiohKB0Blr9mfWt2zyfjjxOIsiTX6Du8TQqGZQ1Nr0+EhEZeXu86E3umlQkPnAmXOBQ1Bp/us2Bjd5kSRc53jqgaBHMto6HApfZvUNAcpQeZoYWe1e5et5zyZD0Uzbc+g+zLfxbhtH2RW2Q66Yq2szodx7mvcBom3X6P+iaheRRA7qgj3cyZoI1q9byyAZRUFsdKVW69+fZT07Qt3n4WRbrO4+JTHy62v2mtEZxMzHMSFXhspkzH3cd5jMARMRP8jw4q8zq9jmPxQlUrmow/PA9FnmIghsIH46wLbX35Kk+FsZx88gawuFCcixgMxHE7IFbulokl5fIZnoXxqeEPrNNfW9HWgBmY6M6kJLK5shgZeZgbBz5aeXuzYRIVnI9iRvLNKDZPz51yLEzMB85Lv0Rbys8eg+odn/82FnJjoXk2IQgwW0EQahSfD18pcniLdrT/8zPiKEnMpvvwxTXeGW0I+C0R2CPqAlECvpuQB3ApmnZobAJ5cPz8qDK+Kl+JDAeDRbnqmbVjB9gq2FbQVvm5AMosA5qOTZ53UTCr9jZYj8SzmyE4NYQAEhvsgS3btaHg/rb72Creoe4TXiaPnSC4IZB9VnYyR61Xer3OiJ4cdn9/GlVSXW0SjlEsJx470PCnMK48V/EWr+Hx1bF0l3WBY9++LR7KXMC5PWxF1prcUAOMEnOMrwYTDwwXceG9N9r8VVQHFkleF+9Durk7RpdiyoLqagGxKSjBmb80ulhBddB8tyR+LW/tJeSGSUgDYVZ3OB+YP1jYKjuTDiezmRvjiKekwa6TsMuWI1VwXl3Rqz3H1AAkXIOn+A+Kufamk4K24PbGcbsDGvzKV4FCUv2gIugMgvw4bfaSZ05fUSOxJGK8ebHaOfkG1eOMfHzoO0Rl/0ep6YwLzNNR2ULXNZ81X2a5fU6dJ5ahA0ko3BB10oeKVC7koGpNNZZgaGHBspaarvm6ex3G+7WeUPpNFcP8anyLsWneqArZl/y5axTHFY4IeXLwyyq9CBYdKcUamlXKZ3Hec8VS2+C9a33cMlTvk/6yLLhnmhXyYHN7lraOJRY8j93rOu8IHVN0QYZhOhL6gDpwioI1kIqkYOoEiczPO1IzxgISN+5RliiT5DqLBCHCqZ/PNfZ5Rd4hJdxzL1rTujG4nItQ+CInVcQ/QI5pJePknJAE8nWzTZboka55nNmH0qOgOEWPEAMbbdp/tKwutynM5v1Rdxqw1r+hHxbCl+QO9Um7XV+ArsD2LvcL83fgWTBZkbp7wt/gQW2bWLmG3TfyPxas58vkby+JFn93jWR2z1fMEgFEkcbXy2NoIcNd02PzzRwEhTJxKP695Ne7OINgShLmaCMyvADpBzf5I5O7OVwS1qCX1ypoDCwXqXxHwMGfIkpGz0Ta7N/GB6TLP0ZzExNv5FMznUo+t0ar2re95ZPqvfbdiO66CjbcS/SRaH//9AeyUnHZvQtQ1JGsJcyZs4YRiouWYkhCkVr5o3iVY1u3/beDScuSauuOxQ8VdS88HDPuYLwAIgjPkXLgmr5q3IcyJjZoZEMUi0zyFhPhfxJXFbkzggUSvXak4LNADOwj/D6YxlSbJ3m96EN/R8Yzq9qo1hAkf/FO/Two8DTk5QrzIoE1tVfZFCvGqd/LjwbLBy1k+84emDfI93LyUtEg35mParjNwmpltO3FLpWQ7Pn+KPhkyAX5xw0AaVpjQ0geE+K/ZHTWjTzUN0UrVQ7rfBg1zHUIpzLcki4lGYvOOQAP1ZUoNSR8Q6P3C2YjNLCwa+D39uA/djv2S6IfxONs9Fc4WnQ7JbOsZCTcAXkyfJi24kvXeX8KNwZFI9reGgYfF3qOpMXET9CgU/MUoJrAngjXPC9kHYdjLRCNDmi5r1pJhUJ+YtDIxS33vZ30CZAUCRy4yPo/kQb4nQZxUSQEybWEZfg96uPajPrTA=="}', + public_key: + '{"alg":"RSA-OAEP-256","e":"AQAB","ext":true,"key_ops":["encrypt"],"kty":"RSA","n":"qVLMv2Iwm_Hyhlnh4etNlHEiCXBzJWbqMwOZ6pz_JuZY2HiqbLmSfBtpvwBKZvcmP92BqLl-qZuLV_bJD_11UBS3gR6ykgU-RsTz1L-V8vA2QZEyULP4DqkMcRCV_7WE_sn_ScePgKszx1284gnngct_1Tv37zB6Ifz7gb1THRwAqOGcE2htea4yQEhyX8ZAl_-95DTWLbXqEAuofqDpXMcQo487VezBWIaDdfw2VX0qi6kM-pt03Gx8uMniyAjhK1G8Dro3wgAtz4PNIwOsdXEvWTSyoXLVMsIuZeO9OGdJKXnZFtVEMzXLyQTD1LjXlsM_TF09fbkN41Tz12ojmQ"}', + }, + client: { + private_key: + '{"alg":"RSA-OAEP-256","d":"IpABtkEzPenNwQng105CKD5NndKj1msi_CXMibzhQk37rbg3xXi9w3KPC8th5JGnb5rl6AxxI-rZrytzUD3C8AVCjes3tSG33BdA1FkFITFSSeD6_ck2pbtxDDVAARHK431VDHjdPHz11Ui3kQZHiNGCtwKGMf9Zts1eg1WjfQnQw2ta4-38mwHpq-4Cm_F1brNTTAu5XlHMws4-TDlYhY3nFU2XvoiR2RPDbMddtvXpDZIVo9s7h3jcS4JxHeJd7mWfwcR_Wf0ArRJIhckgPQtTAAjADNpw_HAdERfJyOAJUnxtHkv4uTu_k23qDpPGEi8euFpQ_1UD8B_Z1Rxylw","dp":"OS3zu_VYJZmOXl1dRXxYGP69MR4YQ3TFJ58HFIxvebD060byGHL-mwf0R6-a1hBkHfSeUI9iPipEcjQeevasPqm5CG8eYMvGU2vhsoq1gfY79rsoKjnThCO3XiUbNeM-G9MRKMRa3ooQ8fUVHyEWKFo1ajoFbVHxZuqTAOgrYT8","dq":"yXtWRU1vM5imQJhIZBt5BO1Rfn-koHTvTM3c5QDdPLyNoGTKTyeoT3P9clN6qevJKTyJJTWiwuz8ZECSksh_m9STCY1ry2HqlF2EKdCZnTQzhoJvb6d7547Witc9eh2rBjsILSxVBadLzOFe8opkkQkdkM_gN_Rr3TtXEAo1vn8","e":"AQAB","ext":true,"key_ops":["decrypt"],"kty":"RSA","n":"qVLMv2Iwm_Hyhlnh4etNlHEiCXBzJWbqMwOZ6pz_JuZY2HiqbLmSfBtpvwBKZvcmP92BqLl-qZuLV_bJD_11UBS3gR6ykgU-RsTz1L-V8vA2QZEyULP4DqkMcRCV_7WE_sn_ScePgKszx1284gnngct_1Tv37zB6Ifz7gb1THRwAqOGcE2htea4yQEhyX8ZAl_-95DTWLbXqEAuofqDpXMcQo487VezBWIaDdfw2VX0qi6kM-pt03Gx8uMniyAjhK1G8Dro3wgAtz4PNIwOsdXEvWTSyoXLVMsIuZeO9OGdJKXnZFtVEMzXLyQTD1LjXlsM_TF09fbkN41Tz12ojmQ","p":"0GJaXeKlxgcz6pX0DdwtWG38x9vN2wfLrN3F8N_0stzyPMjMpLGXOdGq1k1V6FROYvLHZsqdCpziwJ3a1PQaGUg2lO-KeBghlbDk4xfYbzSSPhVdwvUT27dysd3-_TsBvNpVCqCLb9Wgl8f0jrrRmRTSztYSLw3ckL939OJoe0M","q":"0AOMQqdGlz0Tm81uqpzCuQcQLMj-IhmPIMuuTnIU55KCmEwmlf0mkgesj-EEBsC1h6ScC5fvznGNvSGqVQAP5ANNZxGiB73q-2YgH3FpuEeHekufl260E_9tgIuqjtCv-eT_cLUhnRNyuP2ZiqRZsBWLuaQYkTubyGRi6izoofM","qi":"FXbIXivKdh0VBgMtLe5f1OjzyrSW_IfIvz8ZM66F4tUTxnNKk5vSb_q2NPyIOVYbdonuVguX-0VO54Ct16k8VdpQSMmUxGbyQAtIck2IzEzpfbRJgn06wiAI3j8q1nRFhrzhfrpJWVyuTiXBgaeOLWBz8fBpjDU7rptmcoU3tZ4"}', + }, + }, +}; + function generateContext(username: string): IUserState { const date = new Date(); date.setFullYear(date.getFullYear() + 1); @@ -33,6 +53,10 @@ function generateContext(username: string): IUserState { loginToken: token.token, loginExpire: token.when, hashedToken, + ...(e2eeData[username] && { + e2e: e2eeData[username]?.server, + e2ePassword: 'minus mobile dexter forest elvis', + }), }, state: { cookies: [ @@ -77,6 +101,12 @@ function generateContext(username: string): IUserState { name: 'Meteor.userId', value: username, }, + ...(e2eeData[username] && [ + { + name: 'private_key', + value: e2eeData[username]?.client.private_key, + }, + ]), ], }, ], @@ -88,6 +118,7 @@ export const Users = { user1: generateContext('user1'), user2: generateContext('user2'), user3: generateContext('user3'), + userE2EE: generateContext('userE2EE'), samluser1: generateContext('samluser1'), samluser2: generateContext('samluser2'), samluser4: generateContext('samluser4'), From 5228bdb53f4ded413524cc6d77b77d8ba5aad247 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 18 Jun 2024 16:39:06 -0300 Subject: [PATCH 033/112] Add tests for new upload API --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 +- apps/meteor/tests/end-to-end/api/09-rooms.js | 324 ++++++++++++++++++- 2 files changed, 324 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 3addbe8d7f4c..4317ad0dc8ca 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -299,7 +299,6 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); - // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); @@ -633,6 +632,7 @@ test.describe.serial('e2ee support legacy formats', () => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); + // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 5d394b79b120..212ee4cce719 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -300,7 +300,8 @@ describe('[Rooms]', function () { await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); }); - it('should correctly save e2ee file description and properties', async () => { + // Support legacy behavior (not encrypting file) + it('should correctly save file description and properties with type e2e', async () => { await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) @@ -326,6 +327,327 @@ describe('[Rooms]', function () { }); }); + describe('/rooms.media', () => { + let testChannel; + let user; + let userCredentials; + const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; + let blockedMediaTypes; + + before(async () => { + user = await createUser({ joinDefaultChannels: false }); + userCredentials = await login(user.username, password); + testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; + blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList'); + const newBlockedMediaTypes = blockedMediaTypes + .split(',') + .filter((type) => type !== 'image/svg+xml') + .join(','); + await updateSetting('FileUpload_MediaTypeBlackList', newBlockedMediaTypes); + }); + + after(() => + Promise.all([ + deleteRoom({ type: 'c', roomId: testChannel._id }), + deleteUser(user), + updateSetting('FileUpload_Restrict_to_room_members', true), + updateSetting('FileUpload_ProtectFiles', true), + updateSetting('FileUpload_MediaTypeBlackList', blockedMediaTypes), + ]), + ); + + it("don't upload a file to room with file field other than file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('test', imgURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', '[invalid-field]'); + expect(res.body).to.have.property('errorType', 'invalid-field'); + }) + .end(done); + }); + it("don't upload a file to room with empty file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', '') + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', res.body.error); + }) + .end(done); + }); + it("don't upload a file to room with more than 1 file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Just 1 file is allowed'); + }) + .end(done); + }); + + let fileNewUrl; + let fileOldUrl; + let fileId; + it('should upload a PNG file to room', async () => { + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + // expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + // expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + + fileNewUrl = message.file.url; + fileOldUrl = message.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); + fileId = message.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${fileId}/`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('image_type', 'image/png'); + expect(res.body.message.attachments[0]).to.have.property('title', '1024x1024.png'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(2); + expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + }); + }); + + it('should upload a LST file to room', async () => { + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', lstURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('format', 'LST'); + expect(res.body.message.attachments[0]).to.have.property('title', 'lst-test.lst'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(1); + expect(res.body.message.files[0]).to.have.property('name', 'lst-test.lst'); + }); + }); + + it('should not allow uploading a blocked media type to a room', async () => { + await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', lstURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-invalid-file-type'); + }); + }); + + it('should be able to get the file', async () => { + await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should be able to get the file when no access to the room if setting allows it', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', false); + await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should not be able to get the file when no access to the room if setting blocks', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', true); + await request.get(fileNewUrl).set(userCredentials).expect(403); + await request.get(fileOldUrl).set(userCredentials).expect(403); + }); + + it('should be able to get the file if member and setting blocks outside access', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', true); + await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should not be able to get the file without credentials', async () => { + await request.get(fileNewUrl).attach('file', imgURL).expect(403); + await request.get(fileOldUrl).attach('file', imgURL).expect(403); + }); + + it('should be able to get the file without credentials if setting allows', async () => { + await updateSetting('FileUpload_ProtectFiles', false); + await request.get(fileNewUrl).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).expect('Content-Type', 'image/png').expect(200); + }); + + it('should generate thumbnail for SVG files correctly', async () => { + const expectedFileName = `thumb-${svgLogoFileName}`; + let thumbUrl; + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', svgLogoURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + const { files, attachments } = message; + + expect(files).to.be.an('array'); + const hasThumbFile = files.some((file) => file.type === 'image/png' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments.find((attachment) => attachment.title === svgLogoFileName); + expect(thumbAttachment).to.be.an('object'); + thumbUrl = thumbAttachment.image_url; + }); + + await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/png'); + }); + + it('should generate thumbnail for JPEG files correctly', async () => { + const expectedFileName = `thumb-sample-jpeg.jpg`; + let thumbUrl; + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/sample-jpeg.jpg'))) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + const { files, attachments } = message; + + expect(files).to.be.an('array'); + const hasThumbFile = files.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments.find((attachment) => attachment.title === `sample-jpeg.jpg`); + expect(thumbAttachment).to.be.an('object'); + thumbUrl = thumbAttachment.image_url; + }); + + await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); + }); + + // Support legacy behavior (not encrypting file) + it('should correctly save file description and properties with type e2e', async () => { + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .field('t', 'e2e') + .field('description', 'some_file_description') + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('image_type', 'image/png'); + expect(res.body.message.attachments[0]).to.have.property('title', '1024x1024.png'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(2); + expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); + expect(res.body.message).to.have.property('t', 'e2e'); + }); + }); + }); + describe('/rooms.favorite', () => { let testChannel; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; From 9e8363460290e417bb920789634dbcb4e1e5c60a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 09:17:00 -0300 Subject: [PATCH 034/112] Save encrypted content info to the file upload --- apps/meteor/app/api/server/v1/rooms.ts | 20 +++++++++++--- .../app/e2e/client/rocketchat.e2e.room.js | 14 +++++++--- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 10 +++++++ .../client/lib/fileUploadHandler.ts | 4 ++- apps/meteor/client/lib/chats/ChatAPI.ts | 1 + .../client/lib/chats/flows/uploadFiles.ts | 15 +++++++++++ apps/meteor/client/lib/chats/uploads.ts | 7 ++++- .../RoomFiles/components/FileItem.tsx | 26 ++++++++++++++++--- .../server/models/raw/BaseUploadModel.ts | 1 - packages/core-typings/src/IUpload.ts | 4 +++ 10 files changed, 88 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index c05557873ba8..11cc49e853fe 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -221,12 +221,26 @@ API.v1.addRoute( const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 24); + const { fields } = file; + + let content; + + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); + } + } + const details = { name: file.filename, size: fileBuffer.length, type: file.mimetype, rid: this.urlParams.rid, userId: this.userId, + content, expiresAt, }; @@ -239,14 +253,14 @@ API.v1.addRoute( const fileStore = FileUpload.getStore('Uploads'); const uploadedFile = await fileStore.insert(details, fileBuffer); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); - const fileUrl = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); return API.v1.success({ file: { _id: uploadedFile._id, - url: fileUrl, + url: uploadedFile.path, }, }); }, diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 16e431ed7a97..db40280fd645 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -447,6 +447,15 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } + async decryptContent(data) { + if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(data.content.ciphertext); + Object.assign(data, content); + } + + return data; + } + // Decrypt messages async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { @@ -461,10 +470,7 @@ export class E2ERoom extends Emitter { } } - if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { - const content = await this.decrypt(message.content.ciphertext); - Object.assign(message, content); - } + message = await this.decryptContent(message); return { ...message, diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index ec21b47d1c5b..e868db466871 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -510,6 +510,16 @@ class E2E extends Emitter { } } + async decryptFileContent(file: IUploadWithUser): Promise { + const e2eRoom = await this.getInstanceByRoomId(file.rid); + + if (!e2eRoom) { + return file; + } + + return e2eRoom.decryptContent(file); + } + async decryptMessage(message: IMessage | IE2EEMessage): Promise { if (!isE2EEMessage(message) || message.e2e === 'done') { return message; diff --git a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts index e0887888c478..c8e6f90966fd 100644 --- a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts +++ b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts @@ -5,7 +5,9 @@ import { Tracker } from 'meteor/tracker'; Tracker.autorun(() => { const userId = Meteor.userId(); - if (userId) { + // Check for Meteor.loggingIn to be reactive and ensure it will process only after login finishes + // preventing race condition setting the rc_token as null forever + if (userId && Meteor.loggingIn() === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${escape(userId)}; path=/${secure}`; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index b903528f1d34..17650bfdbb8f 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -104,6 +104,7 @@ export type UploadsAPI = { file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fb53cb253eba..bd5a088a41dc 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,6 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi file: File, extraData?: Pick & { description?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ) => { chat.uploads.send( file, @@ -42,6 +43,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ...extraData, }, getContent, + fileContent, ); chat.composer?.clear(); imperativeModal.close(); @@ -143,12 +145,25 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); }; + const fileContentData = { + type: file.type, + typeGroup: file.type.split('/')[0], + name: fileName, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + uploadFile( encryptedFile.file, { t: 'e2e', }, getContent, + fileContent, ); } }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 14c695efd3c0..bcce45231fd9 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -45,6 +45,7 @@ const send = async ( t?: IMessage['t']; }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ): Promise => { const id = Random.id(); @@ -63,6 +64,9 @@ const send = async ( `/v1/rooms.media/${rid}`, { file, + ...(fileContent && { + content: JSON.stringify(fileContent), + }), }, { load: (event) => { @@ -168,5 +172,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string, fileUrl: string) => Promise, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), + fileContent?: IE2EEMessage['content'], + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 3170fe4b2488..28e3a47cce6c 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -1,8 +1,10 @@ +import { Base64 } from '@rocket.chat/base64'; import type { IUpload, IUploadWithUser } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -22,12 +24,28 @@ type FileItemProps = { const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); - const { _id, name, url, uploadedAt, type, typeGroup, user } = fileData; + const [file, setFile] = useState(fileData); + const { _id, path, name, uploadedAt, type, typeGroup, user } = file; + console.log(123, file); + + useEffect(() => { + (async () => { + if (fileData.rid && fileData.content) { + const e2eRoom = await e2e.getInstanceByRoomId(fileData.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(fileData); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + setFile({ ...decrypted }); + } + } + })(); + }, [fileData]); return ( {typeGroup === 'image' ? ( - + ) : ( { display='flex' flexGrow={1} flexShrink={1} - href={url} + href={path} textDecorationLine='none' > diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index d0e187a91b23..4037566272b9 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -88,7 +88,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo }, }; - console.log('confirm', filter, update); return this.updateOne(filter, update); } diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index c40e9d949407..92710b826e12 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -48,6 +48,10 @@ export interface IUpload { Webdav?: { path: string; }; + content?: { + algorithm: string; // 'rc.v1.aes-sha2' + ciphertext: string; // Encrypted subset JSON of IUpload + }; } export type IUploadWithUser = IUpload & { user?: Pick }; From 8f81eddcc84c019b3724e7bf93a1acc0e8dc0b84 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:12:11 -0300 Subject: [PATCH 035/112] Add dimensions to image attachments --- .../client/lib/chats/flows/uploadFiles.ts | 22 +++++++++++++++---- .../RoomFiles/components/FileItem.tsx | 1 - 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index bd5a088a41dc..a3c66bb00ddd 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -21,6 +21,19 @@ if ('serviceWorker' in navigator) { }); } +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; + export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; @@ -108,16 +121,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }; if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + attachments.push({ ...attachment, image_url: fileUrl, image_type: file.type, image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), }); - - // if (file.identify?.size) { - // attachment.image_dimensions = file.identify.size; - // } } else if (/^audio\/.+/.test(file.type)) { attachments.push({ ...attachment, diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 28e3a47cce6c..151a5d1d7d88 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -26,7 +26,6 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); const [file, setFile] = useState(fileData); const { _id, path, name, uploadedAt, type, typeGroup, user } = file; - console.log(123, file); useEffect(() => { (async () => { From b9ff006988bd667f2ddaa64544d56e01a997a40a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:26:23 -0300 Subject: [PATCH 036/112] Fix TS --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index e868db466871..58277fc41d1a 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -1,7 +1,7 @@ import QueryString from 'querystring'; import URL from 'url'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUploadWithUser } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; From 20962c46bbe1399902b61df172f18eea2f140830 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:29:54 -0300 Subject: [PATCH 037/112] Fix tests --- apps/meteor/tests/e2e/fixtures/userStates.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts index 219e40f1ab22..252023eae73c 100644 --- a/apps/meteor/tests/e2e/fixtures/userStates.ts +++ b/apps/meteor/tests/e2e/fixtures/userStates.ts @@ -101,12 +101,14 @@ function generateContext(username: string): IUserState { name: 'Meteor.userId', value: username, }, - ...(e2eeData[username] && [ - { - name: 'private_key', - value: e2eeData[username]?.client.private_key, - }, - ]), + ...(e2eeData[username] + ? [ + { + name: 'private_key', + value: e2eeData[username]?.client.private_key, + }, + ] + : []), ], }, ], From 74cfee1dcde77b49aaf4e5e017cf66509744c27b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 11:18:34 -0300 Subject: [PATCH 038/112] Fix image preview --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 +++ .../components/ImageGallery/ImageGallery.tsx | 4 +-- .../client/lib/chats/flows/uploadFiles.ts | 12 +++++++++ .../room/ImageGallery/hooks/useImagesList.ts | 26 +++++++++++++++---- .../RoomFiles/components/FileItem.tsx | 21 ++------------- .../RoomFiles/hooks/useFilesList.ts | 26 +++++++++++++++---- packages/core-typings/src/IUpload.ts | 4 +++ 7 files changed, 66 insertions(+), 31 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 58277fc41d1a..b170f3fe5e7a 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -511,6 +511,10 @@ class E2E extends Emitter { } async decryptFileContent(file: IUploadWithUser): Promise { + if (!file.rid) { + return file; + } + const e2eRoom = await this.getInstanceByRoomId(file.rid); if (!e2eRoom) { diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 8dd69ab8fc25..2cabfed460bd 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -182,14 +182,14 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onReachBeginning={loadMore} initialSlide={images.length - 1} > - {[...images].reverse().map(({ _id, url }) => ( + {[...images].reverse().map(({ _id, path, url }) => (
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */} - +
diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index a3c66bb00ddd..b44ca3271809 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -154,8 +154,20 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ]; + return e2eRoom.encryptMessageContent({ attachments, + files, + file: files[0], }); }; diff --git a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts index c4f7d0770815..d3db5fbed25b 100644 --- a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts +++ b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts @@ -1,7 +1,9 @@ +import { Base64 } from '@rocket.chat/base64'; import type { RoomsImagesProps } from '@rocket.chat/rest-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useState } from 'react'; +import { e2e } from '../../../../../app/e2e/client/rocketchat.e2e'; import { useScrollableRecordList } from '../../../../hooks/lists/useScrollableRecordList'; import { useComponentDidUpdate } from '../../../../hooks/useComponentDidUpdate'; import { ImagesList } from '../../../../lib/lists/ImagesList'; @@ -40,12 +42,26 @@ export const useImagesList = ( count: end, }); + const items = files.map((file) => ({ + ...file, + uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, + modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, + })); + + for await (const file of items) { + if (file.rid && file.content) { + const e2eRoom = await e2e.getInstanceByRoomId(file.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(file); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + Object.assign(file, decrypted); + } + } + } + return { - items: files.map((file) => ({ - ...file, - uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, - modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, - })), + items, itemCount: total, }; }, diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 151a5d1d7d88..27db74bc2c68 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -1,10 +1,8 @@ -import { Base64 } from '@rocket.chat/base64'; import type { IUpload, IUploadWithUser } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -24,22 +22,7 @@ type FileItemProps = { const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); - const [file, setFile] = useState(fileData); - const { _id, path, name, uploadedAt, type, typeGroup, user } = file; - - useEffect(() => { - (async () => { - if (fileData.rid && fileData.content) { - const e2eRoom = await e2e.getInstanceByRoomId(fileData.rid); - if (e2eRoom?.shouldConvertReceivedMessages()) { - const decrypted = await e2e.decryptFileContent(fileData); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); - decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; - setFile({ ...decrypted }); - } - } - })(); - }, [fileData]); + const { _id, path, name, uploadedAt, type, typeGroup, user } = fileData; return ( diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts index 7875b97576f5..66913204e782 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -1,6 +1,8 @@ +import { Base64 } from '@rocket.chat/base64'; import { useUserRoom, useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useScrollableRecordList } from '../../../../../hooks/lists/useScrollableRecordList'; import { useStreamUpdatesForMessageList } from '../../../../../hooks/lists/useStreamUpdatesForMessageList'; import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate'; @@ -59,12 +61,26 @@ export const useFilesList = ( }), }); + const items = files.map((file) => ({ + ...file, + uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, + modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, + })); + + for await (const file of items) { + if (file.rid && file.content) { + const e2eRoom = await e2e.getInstanceByRoomId(file.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(file); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + Object.assign(file, decrypted); + } + } + } + return { - items: files.map((file) => ({ - ...file, - uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, - modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, - })), + items, itemCount: total, }; }, diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 92710b826e12..b9b367a648f2 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -52,6 +52,10 @@ export interface IUpload { algorithm: string; // 'rc.v1.aes-sha2' ciphertext: string; // Encrypted subset JSON of IUpload }; + encryption?: { + iv: string; + key: JsonWebKey; + }; } export type IUploadWithUser = IUpload & { user?: Pick }; From 6508ebe69fbed7ceff0730dbc3c773ffea6c88f5 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 11:38:06 -0300 Subject: [PATCH 039/112] Prevent keys to be set on top of existent keys --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 5 ++++- .../methods/setUserPublicAndPrivateKeys.ts | 8 ++++++++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 4 ++-- apps/meteor/tests/end-to-end/api/01-users.js | 16 ++++++++++++++++ apps/meteor/tests/end-to-end/api/09-rooms.js | 7 +++---- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index b170f3fe5e7a..346fa986c14e 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -316,7 +316,10 @@ class E2E extends Emitter { this.db_private_key = private_key; } catch (error) { this.setState(E2EEState.ERROR); - return this.error('Error fetching RSA keys: ', error); + this.error('Error fetching RSA keys: ', error); + // Stop any process since we can't communicate with the server + // to get the keys. This prevents new key generation + throw error; } } diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index c00b9b872466..92144d340527 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -19,6 +19,14 @@ Meteor.methods({ }); } + const keys = await Users.fetchKeysByUserId(userId); + + if (keys.private_key && keys.public_key) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } + await Users.setE2EPublicAndPrivateKeysByUserId(userId, { private_key: keyPair.private_key, public_key: keyPair.public_key, diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 4317ad0dc8ca..b64609258d8a 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -616,13 +616,13 @@ test.describe.serial('e2ee room setup', () => { test.describe.serial('e2ee support legacy formats', () => { let poHomeChannel: HomeChannel; + test.use({ storageState: Users.userE2EE.state }); + test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); }); test.beforeAll(async ({ api }) => { - test.use({ storageState: Users.userE2EE.state }); - expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index b1d9a594996b..0fe73ac1d56a 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -73,6 +73,22 @@ describe('[Users]', function () { }); }); + it('should fail when trying to set keys for a user with keys already set', async () => { + await request + .post(api('e2e.setUserPublicAndPrivateKeys')) + .set(userCredentials) + .send({ + private_key: 'test', + public_key: 'test', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Keys already set [error-keys-already-set]'); + }); + }); + describe('[/users.create]', () => { before(async () => clearCustomFields()); after(async () => clearCustomFields()); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 511492810388..d214f44fe808 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -478,7 +478,6 @@ describe('[Rooms]', function () { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - const { message } = res.body; expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('file'); expect(res.body.file).to.have.property('_id'); @@ -486,9 +485,9 @@ describe('[Rooms]', function () { // expect(res.body.message.files[0]).to.have.property('type', 'image/png'); // expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); - fileNewUrl = message.file.url; - fileOldUrl = message.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); - fileId = message.file._id; + fileNewUrl = res.body.file.url; + fileOldUrl = res.body.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); + fileId = res.body.file._id; }); await request From 64a27cb58894c06bcef58f7c66bc37f1530fae4f Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 13:12:03 -0300 Subject: [PATCH 040/112] Fix API tests --- apps/meteor/tests/end-to-end/api/01-users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index 0fe73ac1d56a..ee53820daa2a 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -82,7 +82,7 @@ describe('[Users]', function () { public_key: 'test', }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Keys already set [error-keys-already-set]'); From b5e1ba5685833eb457bbfd76077b4e81f4e191f9 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 13:24:34 -0300 Subject: [PATCH 041/112] Fix e2ee change password --- apps/meteor/app/api/server/v1/e2e.ts | 5 ++++- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 5 +++-- .../server/methods/setUserPublicAndPrivateKeys.ts | 14 ++++++++------ packages/rest-typings/src/v1/e2e.ts | 1 + .../e2eSetUserPublicAndPrivateKeysParamsPOST.ts | 4 ++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 8cb3e8ab4236..4c67e5f4f8b6 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -113,6 +113,8 @@ API.v1.addRoute( * type: string * private_key: * type: string + * force: + * type: boolean * responses: * 200: * content: @@ -135,11 +137,12 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { public_key, private_key } = this.bodyParams; + const { public_key, private_key, force } = this.bodyParams; await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { public_key, private_key, + force, }); return API.v1.success(); diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 346fa986c14e..d2d0dee2198e 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -156,7 +156,7 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string): Promise { + async persistKeys({ public_key, private_key }: KeyPair, password: string, { force = false }): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } @@ -170,6 +170,7 @@ class E2E extends Emitter { await sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', { public_key, private_key: encodedPrivateKey, + force, }); } @@ -300,7 +301,7 @@ class E2E extends Emitter { } async changePassword(newPassword: string): Promise { - await this.persistKeys(this.getKeysFromLocalStorage(), newPassword); + await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); if (Meteor._localStorage.getItem('e2e.randomPassword')) { Meteor._localStorage.setItem('e2e.randomPassword', newPassword); diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index 92144d340527..cd96f77a239f 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string }): void; + 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void; } } @@ -19,12 +19,14 @@ Meteor.methods({ }); } - const keys = await Users.fetchKeysByUserId(userId); + if (!keyPair.force) { + const keys = await Users.fetchKeysByUserId(userId); - if (keys.private_key && keys.public_key) { - throw new Meteor.Error('error-keys-already-set', 'Keys already set', { - method: 'e2e.setUserPublicAndPrivateKeys', - }); + if (keys.private_key && keys.public_key) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } } await Users.setE2EPublicAndPrivateKeysByUserId(userId, { diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index d14cc642fc88..07e9d0379d6a 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -8,6 +8,7 @@ const ajv = new Ajv({ type E2eSetUserPublicAndPrivateKeysProps = { public_key: string; private_key: string; + force?: boolean; }; const E2eSetUserPublicAndPrivateKeysSchema = { diff --git a/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts b/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts index 81ff8ce3c79d..06383408ffd9 100644 --- a/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts +++ b/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts @@ -7,6 +7,7 @@ const ajv = new Ajv({ export type e2eSetUserPublicAndPrivateKeysParamsPOST = { public_key: string; private_key: string; + force?: boolean; }; const e2eSetUserPublicAndPrivateKeysParamsPOSTSchema = { @@ -18,6 +19,9 @@ const e2eSetUserPublicAndPrivateKeysParamsPOSTSchema = { private_key: { type: 'string', }, + force: { + type: 'boolean', + }, }, additionalProperties: false, required: ['public_key', 'private_key'], From f108fd40707a7f83785858bc9109a155683ad601 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 14:05:21 -0300 Subject: [PATCH 042/112] Fix TS --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index d2d0dee2198e..d14f4e07caa1 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -156,7 +156,11 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string, { force = false }): Promise { + async persistKeys( + { public_key, private_key }: KeyPair, + password: string, + { force }: { force: boolean } = { force: false }, + ): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } From ae76481eea6e4cbb78f30a34daca46c78cbfb034 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 14:54:59 -0300 Subject: [PATCH 043/112] Fix API tests --- apps/meteor/reporters/rocketchat.ts | 2 +- apps/meteor/tests/end-to-end/api/09-rooms.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/meteor/reporters/rocketchat.ts b/apps/meteor/reporters/rocketchat.ts index d27424a1874b..2e0ca2333f84 100644 --- a/apps/meteor/reporters/rocketchat.ts +++ b/apps/meteor/reporters/rocketchat.ts @@ -36,7 +36,7 @@ class RocketChatReporter implements Reporter { draft: this.draft, run: this.run, }; - console.log(`Sending test result to Rocket.Chat: ${JSON.stringify(payload)}`); + // console.log(`Sending test result to Rocket.Chat: ${JSON.stringify(payload)}`); return fetch(this.url, { method: 'POST', body: JSON.stringify(payload), diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index d214f44fe808..de4668e86f48 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -374,7 +374,6 @@ describe('[Rooms]', function () { await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) - .field('t', 'e2e') .field('description', 'some_file_description') .attach('file', imgURL) .expect('Content-Type', 'application/json') @@ -391,7 +390,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('type', 'image/png'); expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); - expect(res.body.message).to.have.property('t', 'e2e'); }); }); }); @@ -491,7 +489,7 @@ describe('[Rooms]', function () { }); await request - .post(api(`rooms.mediaConfirm/${fileId}/`)) + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -545,7 +543,7 @@ describe('[Rooms]', function () { }); it('should not allow uploading a blocked media type to a room', async () => { - await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await updateSetting('FileUpload_MediaTypeBlackList', 'text/plain'); await request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) @@ -695,8 +693,9 @@ describe('[Rooms]', function () { await request .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) - .field('t', 'e2e') - .field('description', 'some_file_description') + .send({ + description: 'some_file_description', + }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { @@ -711,7 +710,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('type', 'image/png'); expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); - expect(res.body.message).to.have.property('t', 'e2e'); }); }); }); From 18d0e87dea0ff5305910d25929b7004e7d12ef98 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 17:18:53 -0300 Subject: [PATCH 044/112] Decrypt room's last message correctly --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index db40280fd645..4903dc012668 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -186,20 +186,20 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - const data = await (subscription.lastMessage?.msg && this.decrypt(subscription.lastMessage.msg)); - if (!data?.text) { + if (subscription.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } + const message = await this.decryptMessage(subscription.lastMessage); + Subscriptions.update( { _id: subscription._id, }, { $set: { - 'lastMessage.msg': data.text, - 'lastMessage.e2e': 'done', + lastMessage: message, }, }, ); From a96fa613ab4ebde29eca71912055c4a65f8d306d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 18:32:13 -0300 Subject: [PATCH 045/112] Try to fix tests --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 3e79caeb00da..9f87d0e9395e 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -679,8 +679,6 @@ test.describe.serial('e2ee room setup', () => { test.describe.serial('e2ee support legacy formats', () => { let poHomeChannel: HomeChannel; - test.use({ storageState: Users.userE2EE.state }); - test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); }); @@ -697,6 +695,8 @@ test.describe.serial('e2ee support legacy formats', () => { // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + await restoreState(page, Users.userE2EE); + const channelName = faker.string.uuid(); await poHomeChannel.sidenav.createEncryptedChannel(channelName); From fa2456ae6eea0e7b9e955b850493c9eae3cc82f7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 22:16:25 -0300 Subject: [PATCH 046/112] Fix tests --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 9f87d0e9395e..c1295d9ad228 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -695,6 +695,8 @@ test.describe.serial('e2ee support legacy formats', () => { // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + await page.goto('/home'); + await restoreState(page, Users.userE2EE); const channelName = faker.string.uuid(); From 365cef813b599e812e316d2c52d1f2caf950d2c6 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 15:31:47 -0300 Subject: [PATCH 047/112] Fix preview of encrypted files --- .../message/hooks/useNormalizedMessage.ts | 8 ++++- .../room/ImageGallery/hooks/useImagesList.ts | 8 ++++- .../RoomFiles/hooks/useFilesList.ts | 8 ++++- apps/meteor/public/enc.js | 31 ++++++++++++++++--- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 6898c55ebad2..2ae8a37b70f5 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -39,7 +39,13 @@ export const useNormalizedMessage = (message: TMessag return attachment; } - const key = Base64.encode(JSON.stringify(attachment.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...attachment.encryption, + name: normalizedMessage.file?.name, + type: normalizedMessage.file?.type, + }), + ); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts index d3db5fbed25b..5c0fa39448a1 100644 --- a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts +++ b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts @@ -53,7 +53,13 @@ export const useImagesList = ( const e2eRoom = await e2e.getInstanceByRoomId(file.rid); if (e2eRoom?.shouldConvertReceivedMessages()) { const decrypted = await e2e.decryptFileContent(file); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...decrypted.encryption, + name: decrypted.name, + type: decrypted.type, + }), + ); decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; Object.assign(file, decrypted); } diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts index 66913204e782..f5450faa6754 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -72,7 +72,13 @@ export const useFilesList = ( const e2eRoom = await e2e.getInstanceByRoomId(file.rid); if (e2eRoom?.shouldConvertReceivedMessages()) { const decrypted = await e2e.decryptFileContent(file); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...decrypted.encryption, + name: decrypted.name, + type: decrypted.type, + }), + ); decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; Object.assign(file, decrypted); } diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index ea296f031e8a..4f7564b44274 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -20,16 +20,19 @@ const decrypt = async (key, iv, file) => { return result; }; + const getUrlParams = (url) => { const urlObj = new URL(url); const k = base64DecodeString(urlObj.searchParams.get('key')); - const { key, iv } = JSON.parse(k); + urlObj.searchParams.delete('key'); + + const { key, iv, name, type } = JSON.parse(k); const newUrl = urlObj.href.replace('/file-decrypt/', '/'); - return { key, iv, url: newUrl }; + return { key, iv, url: newUrl, name, type }; }; self.addEventListener('fetch', (event) => { @@ -38,9 +41,11 @@ self.addEventListener('fetch', (event) => { } try { - const { url, key, iv } = getUrlParams(event.request.url); + const { url, key, iv, name, type } = getUrlParams(event.request.url); - const requestToFetch = new Request(url, event.request); + const requestToFetch = new Request(url, { + ...event.request, + }); event.respondWith( caches.match(requestToFetch).then((response) => { @@ -51,8 +56,24 @@ self.addEventListener('fetch', (event) => { return fetch(requestToFetch) .then(async (res) => { const file = await res.arrayBuffer(); + + if (res.status !== 200 || file.byteLength === 0) { + console.error('Failed to fetch file', { req: requestToFetch, res }); + return res; + } + const result = await decrypt(key, iv, file); - const response = new Response(result); + + const newHeaders = new Headers(res.headers); + newHeaders.set('Content-Disposition', 'inline; filename="'+name+'"'); + newHeaders.set('Content-Type', type); + + const response = new Response(result, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + await caches.open('v1').then((cache) => { cache.put(requestToFetch, response.clone()); }); From 81dad69fee6c93f19ec01e13e4ccd0ae48ec4e9a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 15:47:28 -0300 Subject: [PATCH 048/112] Fix download button --- apps/meteor/public/enc.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 4f7564b44274..48778063cdcb 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -22,7 +22,7 @@ const decrypt = async (key, iv, file) => { }; const getUrlParams = (url) => { - const urlObj = new URL(url); + const urlObj = new URL(url, location.origin); const k = base64DecodeString(urlObj.searchParams.get('key')); @@ -97,7 +97,8 @@ self.addEventListener('message', async (event) => { if (event.data.type !== 'attachment-download') { return; } - const requestToFetch = new Request(url); + + const requestToFetch = new Request(event.data.url); const { url, key, iv } = getUrlParams(event.data.url); const res = (await caches.match(requestToFetch)) ?? (await fetch(url)); @@ -109,13 +110,13 @@ self.addEventListener('message', async (event) => { id: event.data.id, type: 'attachment-download-result', result, - }) - .catch((error) => { - console.error('Posting message failed:', error); - event.source.postMessage({ - id: event.data.id, - type: 'attachment-download-result', - error, - }); }); + // .catch((error) => { + // console.error('Posting message failed:', error); + // event.source.postMessage({ + // id: event.data.id, + // type: 'attachment-download-result', + // error, + // }); + // }); }); From 8402305f36c3d4dec87395258c298739dc967551 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 16:49:00 -0300 Subject: [PATCH 049/112] Fix download from files list --- .../RoomFiles/components/FileItem.tsx | 4 ++ .../RoomFiles/components/FileItemMenu.tsx | 41 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 27db74bc2c68..a13014fb7834 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -3,6 +3,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; import React from 'react'; +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -24,6 +25,8 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); const { _id, path, name, uploadedAt, type, typeGroup, user } = fileData; + const encryptedAnchorProps = useDownloadFromServiceWorker(path || '', name); + return ( {typeGroup === 'image' ? ( @@ -41,6 +44,7 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { flexShrink={1} href={path} textDecorationLine='none' + {...(path?.includes('/file-decrypt/') ? encryptedAnchorProps : {})} > diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx index 459002dd8358..157df8d78027 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx @@ -1,10 +1,12 @@ import type { IUpload } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; import { Box, Menu, Icon } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useUserId } from '@rocket.chat/ui-contexts'; -import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react'; import { getURL } from '../../../../../../app/utils/client'; -import { download } from '../../../../../lib/download'; +import { download, downloadAs } from '../../../../../lib/download'; import { useRoom } from '../../../contexts/RoomContext'; import { useMessageDeletionIsAllowed } from '../hooks/useMessageDeletionIsAllowed'; @@ -13,11 +15,33 @@ type FileItemMenuProps = { onClickDelete: (id: IUpload['_id']) => void; }; +const ee = new Emitter>(); + +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; + + ee.emit(event.data.id, { result, id: event.data.id }); + } +}); + const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { const t = useTranslation(); const room = useRoom(); - const uid = useUserId(); - const isDeletionAllowed = useMessageDeletionIsAllowed(room._id, fileData, uid); + const userId = useUserId(); + const isDeletionAllowed = useMessageDeletionIsAllowed(room._id, fileData, userId); + + const { controller } = navigator.serviceWorker; + + const uid = useUniqueId(); + + useEffect( + () => + ee.once(uid, ({ result }) => { + downloadAs({ data: [new Blob([result])] }, fileData.name ?? t('Download')); + }), + [fileData, t, uid], + ); const menuOptions = { downLoad: { @@ -28,6 +52,15 @@ const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { ), action: () => { + if (fileData.path?.includes('/file-decrypt/')) { + controller?.postMessage({ + type: 'attachment-download', + url: fileData.path, + id: uid, + }); + return; + } + if (fileData.url && fileData.name) { const URL = window.webkitURL ?? window.URL; const href = getURL(fileData.url); From 09e3bb70cb7d3194674791669ea2a8bdf8c22a75 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 17:33:04 -0300 Subject: [PATCH 050/112] Force cors on SW --- apps/meteor/public/enc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 48778063cdcb..2a0bcb9db83d 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -45,6 +45,7 @@ self.addEventListener('fetch', (event) => { const requestToFetch = new Request(url, { ...event.request, + mode: 'cors', }); event.respondWith( From b739659579ac1aa21f65b7adb07e0fdd83ab8ae6 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 29 Jun 2024 15:49:38 +0530 Subject: [PATCH 051/112] feat: multiple file sharing feature with encryption --- .../app/api/server/lib/getUploadFormData.ts | 22 +- apps/meteor/app/api/server/v1/rooms.ts | 119 ++++++---- .../server/methods/sendFileMessage.ts | 218 +++++++++--------- apps/meteor/client/lib/chats/ChatAPI.ts | 4 +- .../client/lib/chats/flows/uploadFiles.ts | 160 +++++++------ apps/meteor/client/lib/chats/uploads.ts | 24 +- .../modals/FileUploadModal/FilePreview.tsx | 11 +- .../FileUploadModal/FileUploadModal.tsx | 130 ++++++++--- .../modals/FileUploadModal/GenericPreview.tsx | 20 +- ee/packages/api-client/src/index.ts | 17 +- packages/rest-typings/src/v1/rooms.ts | 3 +- 11 files changed, 434 insertions(+), 294 deletions(-) diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 85fc0658542d..e2157aa56240 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -28,9 +28,9 @@ export async function getUploadFormData< validate?: V; sizeLimit?: number; } = {}, -): Promise> { +): Promise<(UploadResult | undefined)[]> { const limits = { - files: 1, + files: 6, ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; @@ -38,8 +38,9 @@ export async function getUploadFormData< const fields = Object.create(null) as K; let uploadedFile: UploadResult | undefined; + let uploadedArray: (UploadResult | undefined)[] = []; - let returnResult = (_value: UploadResult) => { + let returnResult = (_values: (UploadResult | undefined)[]) => { // noop }; let returnError = (_error?: Error | string | null | undefined) => { @@ -51,13 +52,21 @@ export async function getUploadFormData< } function onEnd() { + uploadedArray.forEach((file) => { + if (!file) { + return returnError(new MeteorError('No file uploaded')); + } + if (options.validate !== undefined && !options.validate(fields)) { + return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); + } + }); if (!uploadedFile) { return returnError(new MeteorError('No file uploaded')); } - if (options.validate !== undefined && !options.validate(fields)) { - return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); + if (uploadedArray.length < 1) { + return returnError(new MeteorError('No file uploaded')); } - return returnResult(uploadedFile); + return returnResult(uploadedArray); } function onFile( @@ -90,6 +99,7 @@ export async function getUploadFormData< fields, fileBuffer: Buffer.concat(fileChunks), }; + uploadedArray.push(uploadedFile); }); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 11cc49e853fe..198c8e5885d5 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -205,62 +205,68 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const file = await getUploadFormData( + const file1 = await getUploadFormData( { request: this.request, }, { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, ); + let file = file1[0]; + let uploadedfilearray = []; + let uploadedfileurlarray = []; + + for (let i = 0; i < file1.length; i++) { + file = file1[i]; + if (!file) { + throw new Meteor.Error('invalid-field'); + } - if (!file) { - throw new Meteor.Error('invalid-field'); - } - - let { fileBuffer } = file; + let { fileBuffer } = file; - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 24); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); - const { fields } = file; + const { fields } = file; - let content; + let content; - if (fields.content) { - try { - content = JSON.parse(fields.content); - } catch (e) { - console.error(e); - throw new Meteor.Error('invalid-field-content'); + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + throw new Meteor.Error('invalid-field-content'); + } } - } - const details = { - name: file.filename, - size: fileBuffer.length, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - content, - expiresAt, - }; - - const stripExif = settings.get('Message_Attachments_Strip_Exif'); - if (stripExif) { - // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) - fileBuffer = await Media.stripExifFromBuffer(fileBuffer); - } + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, fileBuffer); + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } - uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + uploadedfilearray.push(uploadedFile); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + uploadedfileurlarray.push(uploadedFile.path); + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + } return API.v1.success({ file: { - _id: uploadedFile._id, - url: uploadedFile.path, + _id: uploadedfilearray, + url: uploadedfileurlarray, }, }); }, @@ -275,25 +281,38 @@ API.v1.addRoute( if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.unauthorized(); } - - const file = await Uploads.findOneById(this.urlParams.fileId); - - if (!file) { + const filesarray: Partial[] = []; + if (this.bodyParams.fileIds != undefined) { + for (let i = 0; i < this.bodyParams.fileIds.length; i++) { + const fileid = this.bodyParams?.fileIds && this.bodyParams?.fileIds[i]; + const file = await Uploads.findOneById(fileid || this.urlParams.fileId); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + filesarray.push(file); + } + } else { throw new Meteor.Error('invalid-file'); } - - file.description = this.bodyParams.description; - delete this.bodyParams.description; - + if (filesarray[0] != null) { + const file = await filesarray[0]; + file.description = this.bodyParams?.description; + delete this.bodyParams.description; + } + delete this.bodyParams.fileIds; await sendFileMessage( this.userId, - { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { roomId: this.urlParams.rid, file: filesarray, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }, ); - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); - - const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + for (let i = 0; i < this.urlParams.fileId.length; i++) { + await Uploads.confirmTemporaryFile(this.urlParams.fileId[i], this.userId); + } + let message; + if (filesarray[0] != null && filesarray[0]._id != undefined) { + message = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, this.userId); + } return API.v1.success({ message, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 485528a5e62f..3b5e0e4edb92 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,6 +6,7 @@ import type { AtLeast, FilesAndAttachments, IMessage, + FileProp, } from '@rocket.chat/core-typings'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -30,121 +31,124 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL } export const parseFileIntoMessageAttachments = async ( - file: Partial, + filearr: Partial[], roomId: string, user: IUser, ): Promise => { - validateFileRequiredFields(file); - - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - const attachments: MessageAttachment[] = []; - - const files = [ - { - _id: file._id, - name: file.name || '', - type: file.type || 'file', - size: file.size || 0, - format: file.identify?.format || '', - }, - ]; - - if (/^image\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file?.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type as string, - image_size: file.size, - }; - - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; - } - - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); + const filesarray: FileProp[] = []; + filearr.forEach(async (file: Partial) => { + validateFileRequiredFields(file); + + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + + const files = [ + { + _id: file._id, + name: file.name || '', + type: file.type || 'file', + size: file.size || 0, + format: file.identify?.format || '', + }, + ]; + + if (/^image\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type as string, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; } - } catch (e) { - SystemLogger.error(e); + + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // } + // } catch (e) { + // SystemLogger.error(e); + // } + attachments.push(attachment); + } else if (/^audio\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type as string, + audio_size: file.size, + }; + attachments.push(attachment); + } else if (/^video\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type as string, + video_size: file.size as number, + }; + attachments.push(attachment); + } else { + const attachment = { + title: file.name, + type: 'file', + format: getFileExtension(file.name), + description: file.description, + title_link: fileUrl, + title_link_download: true, + size: file.size as number, + }; + attachments.push(attachment); } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type as string, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type as string, - video_size: file.size as number, - }; - attachments.push(attachment); - } else { - const attachment = { - title: file.name, - type: 'file', - format: getFileExtension(file.name), - description: file.description, - title_link: fileUrl, - title_link_download: true, - size: file.size as number, - }; - attachments.push(attachment); - } - return { files, attachments }; + filesarray.push(...files); + }); + return { files: filesarray, attachments }; }; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendFileMessage: (roomId: string, _store: string, file: Partial, msgData?: Record) => boolean; + sendFileMessage: (roomId: string, _store: string, file: Partial[], msgData?: Record) => boolean; } } @@ -156,7 +160,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial; + file: Partial[]; msgData?: Record; }, { @@ -182,7 +186,6 @@ export const sendFileMessage = async ( if (user?.type !== 'app' && !(await canAccessRoomAsync(room, user))) { return false; } - check( msgData, Match.Maybe({ @@ -202,7 +205,6 @@ export const sendFileMessage = async ( ), }), ); - const data = { rid: roomId, ts: new Date(), @@ -211,7 +213,6 @@ export const sendFileMessage = async ( msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, }; - if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; @@ -234,7 +235,6 @@ Meteor.methods({ method: 'sendFileMessage', } as any); } - return sendFileMessage(userId, { roomId, file, msgData }); }, }); diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 3fa159ad1c3b..dafb5d49a305 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -101,9 +101,9 @@ export type UploadsAPI = { wipeFailedOnes(): void; cancel(id: Upload['id']): void; send( - file: File, + file: File[] | File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index b44ca3271809..b8ac332c3e9a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -43,10 +43,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; + function updateQueue(finalFiles: File[]) { + queue.length = 0; + finalFiles.forEach((file) => { + queue.push(file); + }); + } + const uploadFile = ( - file: File, + file: File[] | File, extraData?: Pick & { description?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ) => { chat.uploads.send( @@ -60,11 +67,9 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ); chat.composer?.clear(); imperativeModal.close(); - uploadNextFile(); }; - const uploadNextFile = (): void => { - const file = queue.pop(); + const file = queue[0]; if (!file) { chat.composer?.dismissAllQuotedMessages(); return; @@ -74,12 +79,13 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi component: FileUploadModal, props: { file, + queue, + updateQueue, fileName: file.name, fileDescription: chat.composer?.text ?? '', showDescription: room && !isRoomFederated(room), onClose: (): void => { imperativeModal.close(); - uploadNextFile(); }, onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { @@ -87,104 +93,110 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi value: fileName, }); - // encrypt attachment description const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { description }); + uploadFile(queue, { description }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { description }); + uploadFile(queue, { description }); return; } - const encryptedFile = await e2eRoom.encryptFile(file); - - if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string): Promise => { + const encryptedFilesarray: any = await Promise.all( + queue.map(async (file) => { + return await e2eRoom.encryptFile(file); + }), + ); + const filesarray = encryptedFilesarray.map((file: any) => { + return file?.file; + }); + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; - - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description, - title_link: fileUrl, - title_link_download: true, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - }; - - if (/^image\/.+/.test(file.type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - - attachments.push({ - ...attachment, - image_url: fileUrl, - image_type: file.type, - image_size: file.size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl, - audio_type: file.type, - audio_size: file.size, - }); - } else if (/^video\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - video_url: fileUrl, - video_type: file.type, - video_size: file.size, - }); - } else { - attachments.push({ - ...attachment, - size: file.size, - // format: getFileExtension(file.name), - }); - } - - const files = [ - { - _id, - name: file.name, - type: file.type, - size: file.size, + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: queue[i].name, + type: 'file', + description, + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + }; + + if (/^image\/.+/.test(queue[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(queue[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: queue[i].type, + image_size: queue[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(queue[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: queue[i].type, + audio_size: queue[i].size, + }); + } else if (/^video\/.+/.test(queue[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: queue[i].type, + video_size: queue[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: queue[i].size, + // format: getFileExtension(file.name), + }); + } + + const files = { + _id: _id[i], + name: queue[i].name, + type: queue[i].type, + size: queue[i].size, // "format": "png" - }, - ]; + }; + arrayoffiles.push(files); + } return e2eRoom.encryptMessageContent({ attachments, - files, + files: arrayoffiles, file: files[0], }); }; const fileContentData = { - type: file.type, - typeGroup: file.type.split('/')[0], + type: queue[0].type, + typeGroup: queue[0].type.split('/')[0], name: fileName, encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, }, }; const fileContent = await e2eRoom.encryptMessageContent(fileContentData); uploadFile( - encryptedFile.file, + filesarray, { t: 'e2e', }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index bcce45231fd9..bc722bd4fd66 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -30,7 +30,7 @@ const wipeFailedOnes = (): void => { }; const send = async ( - file: File, + file: File[] | File, { description, msg, @@ -44,16 +44,18 @@ const send = async ( tmid?: string; t?: IMessage['t']; }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise => { + if (!Array.isArray(file)) { + file = [file]; + } const id = Random.id(); - updateUploads((uploads) => [ ...uploads, { id, - name: file.name, + name: file[0].name || file[0]?.file?.name, percentage: 0, }, ]); @@ -116,17 +118,21 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - let content; + let content: IE2EEMessage['content']; + let fileIds: string[] = result.file._id.map((file: any) => file._id); + let fileUrlarray: string[] = result.file.url; + if (getContent) { - content = await getContent(result.file._id, result.file.url); + content = await getContent(fileIds, fileUrlarray); } - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { msg, tmid, description, t, content, + fileIds, }); } }; @@ -169,9 +175,9 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes wipeFailedOnes, cancel, send: ( - file: File, + file: File[] | File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index c898c8b0081d..72f2308ffeda 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -41,16 +41,23 @@ const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefine type FilePreviewProps = { file: File; + key: number; + index: number; + onRemove: (index: number) => void; }; -const FilePreview = ({ file }: FilePreviewProps): ReactElement => { +const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { const fileType = getFileType(file.type); + const handleRemove = () => { + onRemove(index); + }; + if (shouldShowMediaPreview(file, fileType)) { return ; } - return ; + return ; }; export default FilePreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 312776397b32..d15fbf41c1c4 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,4 +1,17 @@ -import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldError, TextInput, Button } from '@rocket.chat/fuselage'; +import { + Modal, + Box, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + TextInput, + Button, + Scrollable, + Tile, + Icon, +} from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -9,8 +22,10 @@ import FilePreview from './FilePreview'; type FileUploadModalProps = { onClose: () => void; + queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; + updateQueue: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -19,6 +34,8 @@ type FileUploadModalProps = { const FileUploadModal = ({ onClose, + queue = [], + updateQueue, file, fileName, fileDescription, @@ -26,7 +43,6 @@ const FileUploadModal = ({ invalidContentType, showDescription = true, }: FileUploadModalProps): ReactElement => { - const [name, setName] = useState(fileName); const [description, setDescription] = useState(fileDescription || ''); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -34,35 +50,84 @@ const FileUploadModal = ({ const ref = useAutoFocus(); - const handleName = (e: ChangeEvent): void => { - setName(e.currentTarget.value); - }; - const handleDescription = (e: ChangeEvent): void => { setDescription(e.currentTarget.value); }; + const [queue1, setQueue1] = useState(queue); - const handleSubmit: FormEventHandler = (e): void => { + const handleremove = (index: number) => { + const temp = queue1.filter((_, i) => { + return i !== index; + }); + setQueue1(temp); + }; + + const handleAddfile = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.click(); + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + const files = Array.from(target.files as FileList); + setQueue1([...queue1, ...files]); + updateQueue([...queue1, ...files]); + }; + }; + + const handleSubmit: FormEventHandler = async (e): Promise => { e.preventDefault(); - if (!name) { - return dispatchToastMessage({ + if (queue.length > 6) { + dispatchToastMessage({ type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), + message: "You can't upload more than 6 files at once", }); + onClose(); + return; } - // -1 maxFileSize means there is no limit - if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - onClose(); - return dispatchToastMessage({ - type: 'error', - message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), - }); + // Iterate over each file in the queue + for (const queuedFile of queue) { + const { name: queuedFileName, size: queuedFileSize, type: queuedFileType } = queuedFile; + if (!queuedFileName) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + return; + } + + // Validate file size + if (maxFileSize > -1 && (queuedFileSize || 0) > maxFileSize) { + onClose(); + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}+" hello testing"`, + }); + return; + } + + // Validate file content type + if (invalidContentType) { + dispatchToastMessage({ + type: 'error', + message: t('FileUpload_MediaType_NotAccepted__type__', { type: queuedFileType }), + }); + onClose(); + return; + } } + // description, + // msg, // Assuming msg is defined elsewhere + // }); - onSubmit(name, description); - }; + // Clear the composer after each file submission + // chat.composer?.clear(); + onSubmit(fileName, description); + // Close the modal after all files are submitted + // imperativeModal.close(); + }; useEffect(() => { if (invalidContentType) { dispatchToastMessage({ @@ -90,17 +155,13 @@ const FileUploadModal = ({ - - - + + + {queue1.length > 0 && + queue1.map((file, index) => )} + + - - {t('Upload_file_name')} - - - - {!name && {t('error-the-field-is-required', { field: t('Name') })}} - {showDescription && ( {t('Upload_file_description')} @@ -111,12 +172,19 @@ const FileUploadModal = ({ )} - + + + + - diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index dd251bec6769..3d07c8d0f3c4 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,13 +1,23 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; - import { formatBytes } from '../../../../lib/utils/formatBytes'; -const GenericPreview = ({ file }: { file: File }): ReactElement => ( - - - {`${file.name} - ${formatBytes(file.size, 2)}`} +type GenericPreviewProps = { + file: File; + index: number; // Add index as a prop + onRemove: (index: number) => void; // Function to handle file removal with index +}; + +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => ( + + + + {`${file.name} - ${formatBytes(file.size, 2)}`} + + + onRemove(index)} /> {/* Pass index to onRemove */} + ); diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index c1648e87c615..8417a521cb23 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -221,7 +221,6 @@ export class RestClient implements RestClientInterface { } send(endpoint: string, method: string, { headers, ...options }: Omit = {}): Promise { - console.log(`hello ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); return fetch(`${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, { ...options, headers: { ...this.getCredentialsAsHeaders(), ...this.headers, ...headers }, @@ -276,13 +275,21 @@ export class RestClient implements RestClientInterface { const data = new FormData(); Object.entries(params as any).forEach(([key, value]) => { - if (value instanceof File) { + if (Array.isArray(value)) { + value.forEach((file) => { + if (file instanceof File) { + data.append(key, file, file.name); + return; + } + file && data.append(key, file as any); + }); + } else if (value instanceof File) { data.append(key, value, value.name); - return; + } else { + value && data.append(key, value as any); } - value && data.append(key, value as any); }); - console.log(`hello from file ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); + xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => { xhr.setRequestHeader(key, value); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index ee05f10f2c14..069e5bcb26ee 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -622,7 +622,7 @@ export type RoomsEndpoints = { }; '/v1/rooms.media/:rid': { - POST: (params: { file: File }) => { file: { url: string } }; + POST: (params: { file: File | File[] }) => { file: { url: string } }; }; '/v1/rooms.mediaConfirm/:rid/:fileId': { @@ -637,6 +637,7 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; content?: IE2EEMessage['content']; + fileIds?: string[]; }) => { message: IMessage | null }; }; From 74ed1f2621152bc5aaed3642ef05658623a73ef9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 29 Jun 2024 19:15:40 +0530 Subject: [PATCH 052/112] fix: file name issue --- apps/meteor/client/lib/chats/uploads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 49f4ea623057..88c1bad7d9f3 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -55,7 +55,7 @@ const send = async ( ...uploads, { id, - name: fileContent?.raw.name || file[0].name || file[0]?.file?.name, + name: file[0].name || fileContent?.raw.name || file[0]?.file?.name, percentage: 0, }, ]); From c93da3082453d4c274d13da128f5fe10d41eb01a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 7 Jul 2024 15:35:52 +0530 Subject: [PATCH 053/112] multiple files shared one at a time --- .../app/api/server/lib/getUploadFormData.ts | 22 +-- apps/meteor/app/api/server/v1/rooms.ts | 159 +++++++++++++----- .../client/lib/chats/flows/uploadFiles.ts | 15 +- apps/meteor/client/lib/chats/uploads.ts | 98 +++++------ .../room/composer/messageBox/MessageBox.tsx | 1 + .../FileUploadModal/FileUploadModal.tsx | 4 +- 6 files changed, 186 insertions(+), 113 deletions(-) diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index e2157aa56240..85fc0658542d 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -28,9 +28,9 @@ export async function getUploadFormData< validate?: V; sizeLimit?: number; } = {}, -): Promise<(UploadResult | undefined)[]> { +): Promise> { const limits = { - files: 6, + files: 1, ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; @@ -38,9 +38,8 @@ export async function getUploadFormData< const fields = Object.create(null) as K; let uploadedFile: UploadResult | undefined; - let uploadedArray: (UploadResult | undefined)[] = []; - let returnResult = (_values: (UploadResult | undefined)[]) => { + let returnResult = (_value: UploadResult) => { // noop }; let returnError = (_error?: Error | string | null | undefined) => { @@ -52,21 +51,13 @@ export async function getUploadFormData< } function onEnd() { - uploadedArray.forEach((file) => { - if (!file) { - return returnError(new MeteorError('No file uploaded')); - } - if (options.validate !== undefined && !options.validate(fields)) { - return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); - } - }); if (!uploadedFile) { return returnError(new MeteorError('No file uploaded')); } - if (uploadedArray.length < 1) { - return returnError(new MeteorError('No file uploaded')); + if (options.validate !== undefined && !options.validate(fields)) { + return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); } - return returnResult(uploadedArray); + return returnResult(uploadedFile); } function onFile( @@ -99,7 +90,6 @@ export async function getUploadFormData< fields, fileBuffer: Buffer.concat(fileChunks), }; - uploadedArray.push(uploadedFile); }); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3aa410869ed1..878358f317af 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -211,73 +211,143 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const file1 = await getUploadFormData( + const file = await getUploadFormData( { request: this.request, }, { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, ); - let file = file1[0]; - let uploadedfilearray = []; - let uploadedfileurlarray = []; - - for (let i = 0; i < file1.length; i++) { - file = file1[i]; - if (!file) { - throw new Meteor.Error('invalid-field'); - } - let { fileBuffer } = file; + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 24); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); - const { fields } = file; + const { fields } = file; - let content; + let content; - if (fields.content) { - try { - content = JSON.parse(fields.content); - } catch (e) { - throw new Meteor.Error('invalid-field-content'); - } + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); } + } - const details = { - name: file.filename, - size: fileBuffer.length, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - content, - expiresAt, - }; + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; - const stripExif = settings.get('Message_Attachments_Strip_Exif'); - if (stripExif) { - // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) - fileBuffer = await Media.stripExifFromBuffer(fileBuffer); - } + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, fileBuffer); - uploadedfilearray.push(uploadedFile); + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); - uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); - uploadedfileurlarray.push(uploadedFile.path); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); - } return API.v1.success({ file: { - _id: uploadedfilearray, - url: uploadedfileurlarray, + _id: uploadedFile._id, + url: uploadedFile.path, }, }); }, }, ); +// API.v1.addRoute( +// 'rooms.media/:rid', +// { authRequired: true }, +// { +// async post() { +// if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { +// return API.v1.unauthorized(); +// } + +// const file1 = await getUploadFormData( +// { +// request: this.request, +// }, +// { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, +// ); +// let file = file1[0]; +// let uploadedfilearray = []; +// let uploadedfileurlarray = []; + +// for (let i = 0; i < file1.length; i++) { +// file = file1[i]; +// if (!file) { +// throw new Meteor.Error('invalid-field'); +// } + +// let { fileBuffer } = file; + +// const expiresAt = new Date(); +// expiresAt.setHours(expiresAt.getHours() + 24); + +// const { fields } = file; + +// let content; + +// if (fields.content) { +// try { +// content = JSON.parse(fields.content); +// } catch (e) { +// throw new Meteor.Error('invalid-field-content'); +// } +// } + +// const details = { +// name: file.filename, +// size: fileBuffer.length, +// type: file.mimetype, +// rid: this.urlParams.rid, +// userId: this.userId, +// content, +// expiresAt, +// }; + +// const stripExif = settings.get('Message_Attachments_Strip_Exif'); +// if (stripExif) { +// // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) +// fileBuffer = await Media.stripExifFromBuffer(fileBuffer); +// } + +// const fileStore = FileUpload.getStore('Uploads'); +// const uploadedFile = await fileStore.insert(details, fileBuffer); +// uploadedfilearray.push(uploadedFile); + +// uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); +// uploadedfileurlarray.push(uploadedFile.path); + +// await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); +// } +// return API.v1.success({ +// file: { +// _id: uploadedfilearray, +// url: uploadedfileurlarray, +// }, +// }); +// }, +// }, +// ); API.v1.addRoute( 'rooms.mediaConfirm/:rid/:fileId', @@ -305,6 +375,7 @@ API.v1.addRoute( file.description = this.bodyParams?.description; delete this.bodyParams.description; } + // this.bodyParams.msg = '@test1 @test2 File(s) uploaded successfully'; delete this.bodyParams.fileIds; await sendFileMessage( this.userId, diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 67e357278100..9fdb6b5a903a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -87,7 +87,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi onClose: (): void => { imperativeModal.close(); }, - onSubmit: async (fileName: string, description?: string): Promise => { + // onSubmit: async (fileName: string, description?: string): Promise => { + onSubmit: async (fileName: string, msg?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, @@ -96,25 +97,28 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(queue, { description }); + uploadFile(queue, { msg }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(queue, { description }); + uploadFile(queue, { msg }); return; } - + console.log('uploading file', file); + console.log('message ', msg); const encryptedFilesarray: any = await Promise.all( queue.map(async (file) => { return await e2eRoom.encryptFile(file); }), ); + console.log('encryptedFilesarray', encryptedFilesarray); const filesarray = encryptedFilesarray.map((file: any) => { return file?.file; }); + console.log('filesarray', filesarray); if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -123,7 +127,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: queue[i].name, type: 'file', - description, title_link: fileUrl[i], title_link_download: true, encryption: { @@ -187,6 +190,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: queue[0].type, typeGroup: queue[0].type.split('/')[0], name: fileName, + msg: msg || '', encryption: { key: encryptedFilesarray[0].key, iv: encryptedFilesarray[0].iv, @@ -194,6 +198,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }; const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + console.log('fileContent', fileContent); uploadFile( filesarray, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 88c1bad7d9f3..a028ba8a3802 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -60,27 +60,30 @@ const send = async ( }, ]); - try { - await new Promise((resolve, reject) => { + var fileIds: string[] = []; + var fileUrls: string[] = []; + const promisearray: Promise[] = []; + + file.map((f) => { + new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { - file, + file: f, ...(fileContent && { content: JSON.stringify(fileContent.encrypted), }), }, { - load: (event) => { - resolve(event); - }, + // load: () => resolve(), progress: (event) => { if (!event.lengthComputable) { return; } const progress = (event.loaded / event.total) * 100; if (progress === 100) { - return; + resolve(); + // return; } updateUploads((uploads) => @@ -118,55 +121,56 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - let content: IE2EEMessage['content']; - let fileIds: string[] = result.file._id.map((file: any) => file._id); - let fileUrlarray: string[] = result.file.url; - - if (getContent) { - content = await getContent(fileIds, fileUrlarray); + fileIds.push(result.file._id); + fileUrls.push(result.file.url); + if (fileIds.length === file.length) { + try { + let content; + if (getContent) { + content = await getContent(fileIds, fileUrls); + } + + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + msg, + tmid, + description, + t, + content, + fileIds, + }); + + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + } catch (error) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } } - - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - msg, - tmid, - description, - t, - content, - fileIds, - }); + // resolve(); } }; - if (uploads.length) { - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - emitter.once(`cancelling-${id}`, () => { xhr.abort(); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + reject(new Error('Upload cancelled')); }); }); - - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error: unknown) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - } + }); }; export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 4b7a466d6de0..1dcd435b004d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -403,6 +403,7 @@ const MessageBox = ({ {isRecordingVideo && } {isRecordingAudio && } + Date: Wed, 17 Jul 2024 01:30:14 +0530 Subject: [PATCH 054/112] Change message API to handle file upload --- .../app/lib/server/methods/sendMessage.ts | 38 +++++++++++++++++-- apps/meteor/client/lib/chats/uploads.ts | 24 ++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index a490b5c4c67f..dbbbd1b1dc76 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; -import { Messages, Users } from '@rocket.chat/models'; +import { Messages, Uploads, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -14,6 +14,10 @@ import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; +import { canAccessRoomIdAsync } from '/app/authorization/server/functions/canAccessRoom'; +import { API } from '/app/api/server'; +import { IUpload } from '@rocket.chat/core-typings'; +import { sendFileMessage } from '/app/file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -111,12 +115,12 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]): any; + sendMessage(message: AtLeast, previewUrls?: string[], filesToConfirm?: string[], msgData?: any): any; } } Meteor.methods({ - async sendMessage(message, previewUrls) { + async sendMessage(message, previewUrls, filesToConfirm, msgData) { check(message, Object); const uid = Meteor.userId(); @@ -126,6 +130,34 @@ Meteor.methods({ }); } + if (filesToConfirm != undefined) { + if (!(await canAccessRoomIdAsync(message.rid, uid))) { + return API.v1.unauthorized(); + } + const filesarray: Partial[] = []; + for (let i = 0; i < filesToConfirm.length; i++) { + const fileid = filesToConfirm[i]; + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + filesarray.push(file); + } + await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); + + for (let i = 0; i < filesToConfirm.length; i++) { + await Uploads.confirmTemporaryFile(filesToConfirm[i], uid); + } + let resmessage; + if (filesarray[0] != null && filesarray[0]._id != undefined) { + resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); + } + + return API.v1.success({ + resmessage, + }); + } + try { return await executeSendMessage(uid, message, previewUrls); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index a028ba8a3802..07390e0f0c12 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -124,20 +124,36 @@ const send = async ( fileIds.push(result.file._id); fileUrls.push(result.file.url); if (fileIds.length === file.length) { + if (msg == undefined) { + msg = ''; + } + const text: IMessage = { + rid, + msg, + _id: id, + }; try { let content; if (getContent) { content = await getContent(fileIds, fileUrls); } - - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + const msgData = { msg, tmid, description, t, content, - fileIds, - }); + }; + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + // msg, + // tmid, + // description, + // t, + // content, + // fileIds, + // }); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { From 2e344a1ea108763f94e5c8f45ee17601f8b5d52e Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 17 Jul 2024 14:29:47 +0530 Subject: [PATCH 055/112] fix: Ensure correct preview of multiple images in shared in single message --- apps/meteor/client/providers/ImageGalleryProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index e2365e534ca3..02b04dffeeed 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -23,8 +23,10 @@ const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { return setSingleImageUrl(target.dataset.id); } if (target?.classList.contains('gallery-item')) { + const id1 = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; + const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; - return setImageId(target.dataset.id || id); + return setImageId(id1 || target.dataset.id || id); } if (target?.classList.contains('gallery-item-container')) { return setImageId(target.dataset.id); From 671fd31f54534df9b2ed2cc9622d2edd93e203fa Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 18 Jul 2024 12:42:14 +0530 Subject: [PATCH 056/112] solved merge conflict --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 3ba1e642c541..fa7c2d7a610b 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -156,9 +156,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi attachments.push({ ...attachment, size: queue[i].size, - format: getFileExtension(file.name), + format: getFileExtension(queue[i].name), }); } + const files = { _id: _id[i], From 9a97c3d0fe384c373c14372597777862128178f0 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 25 Jul 2024 13:49:24 +0530 Subject: [PATCH 057/112] fix: type error of fileContent and remove extra code --- apps/meteor/app/api/server/v1/rooms.ts | 114 ++---------------- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 2 +- apps/meteor/client/lib/chats/uploads.ts | 15 +-- 4 files changed, 16 insertions(+), 117 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 878358f317af..fc4175c996ac 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -272,82 +272,6 @@ API.v1.addRoute( }, }, ); -// API.v1.addRoute( -// 'rooms.media/:rid', -// { authRequired: true }, -// { -// async post() { -// if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { -// return API.v1.unauthorized(); -// } - -// const file1 = await getUploadFormData( -// { -// request: this.request, -// }, -// { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, -// ); -// let file = file1[0]; -// let uploadedfilearray = []; -// let uploadedfileurlarray = []; - -// for (let i = 0; i < file1.length; i++) { -// file = file1[i]; -// if (!file) { -// throw new Meteor.Error('invalid-field'); -// } - -// let { fileBuffer } = file; - -// const expiresAt = new Date(); -// expiresAt.setHours(expiresAt.getHours() + 24); - -// const { fields } = file; - -// let content; - -// if (fields.content) { -// try { -// content = JSON.parse(fields.content); -// } catch (e) { -// throw new Meteor.Error('invalid-field-content'); -// } -// } - -// const details = { -// name: file.filename, -// size: fileBuffer.length, -// type: file.mimetype, -// rid: this.urlParams.rid, -// userId: this.userId, -// content, -// expiresAt, -// }; - -// const stripExif = settings.get('Message_Attachments_Strip_Exif'); -// if (stripExif) { -// // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) -// fileBuffer = await Media.stripExifFromBuffer(fileBuffer); -// } - -// const fileStore = FileUpload.getStore('Uploads'); -// const uploadedFile = await fileStore.insert(details, fileBuffer); -// uploadedfilearray.push(uploadedFile); - -// uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); -// uploadedfileurlarray.push(uploadedFile.path); - -// await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); -// } -// return API.v1.success({ -// file: { -// _id: uploadedfilearray, -// url: uploadedfileurlarray, -// }, -// }); -// }, -// }, -// ); API.v1.addRoute( 'rooms.mediaConfirm/:rid/:fileId', @@ -357,39 +281,25 @@ API.v1.addRoute( if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = []; - if (this.bodyParams.fileIds != undefined) { - for (let i = 0; i < this.bodyParams.fileIds.length; i++) { - const fileid = this.bodyParams?.fileIds && this.bodyParams?.fileIds[i]; - const file = await Uploads.findOneById(fileid || this.urlParams.fileId); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - filesarray.push(file); - } - } else { + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { throw new Meteor.Error('invalid-file'); } - if (filesarray[0] != null) { - const file = await filesarray[0]; - file.description = this.bodyParams?.description; - delete this.bodyParams.description; - } - // this.bodyParams.msg = '@test1 @test2 File(s) uploaded successfully'; - delete this.bodyParams.fileIds; + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + await sendFileMessage( this.userId, - { roomId: this.urlParams.rid, file: filesarray, msgData: this.bodyParams }, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }, ); - for (let i = 0; i < this.urlParams.fileId.length; i++) { - await Uploads.confirmTemporaryFile(this.urlParams.fileId[i], this.userId); - } - let message; - if (filesarray[0] != null && filesarray[0]._id != undefined) { - message = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, this.userId); - } + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); return API.v1.success({ message, diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 0ef931d46bc9..0fa55d4dbcc1 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -104,7 +104,7 @@ export type UploadsAPI = { file: File[] | File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: IE2EEMessage['content'], + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fa7c2d7a610b..cade8623b30e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -42,7 +42,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi file: File[] | File, extraData?: Pick & { description?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { chat.uploads.send( file, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 07390e0f0c12..fac41d6bbc09 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -45,7 +45,7 @@ const send = async ( t?: IMessage['t']; }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { if (!Array.isArray(file)) { file = [file]; @@ -62,7 +62,6 @@ const send = async ( var fileIds: string[] = []; var fileUrls: string[] = []; - const promisearray: Promise[] = []; file.map((f) => { new Promise((resolve, reject) => { @@ -144,16 +143,7 @@ const send = async ( t, content, }; - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - // msg, - // tmid, - // description, - // t, - // content, - // fileIds, - // }); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { @@ -176,7 +166,6 @@ const send = async ( } } } - // resolve(); } }; @@ -198,6 +187,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes file: File[] | File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); From 62fbc56ea7a6cb80e7580019c26ccd44b25e784c Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 31 Jul 2024 08:24:55 +0530 Subject: [PATCH 058/112] added support for single file upload --- .../app/file-upload/server/methods/sendFileMessage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 0ede869af08d..d683fac8fdbc 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -31,12 +31,15 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL } export const parseFileIntoMessageAttachments = async ( - filearr: Partial[], + filearr: Partial[] | Partial, roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; const filesarray: FileProp[] = []; + if (!Array.isArray(filearr)) { + filearr = [filearr]; + } filearr.forEach(async (file: Partial) => { validateFileRequiredFields(file); @@ -160,7 +163,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial[]; + file: Partial[] | Partial; msgData?: Record; }, { From f88cd2610ae0590cf76e61ba27bd1028de8834f9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 1 Aug 2024 01:00:48 +0530 Subject: [PATCH 059/112] UI update added cross button on hover and added the functionality of removing files in image also --- .../modals/FileUploadModal/FilePreview.tsx | 4 +- .../modals/FileUploadModal/GenericPreview.tsx | 71 +++++++++++++++---- .../modals/FileUploadModal/ImagePreview.tsx | 33 +++++++-- .../modals/FileUploadModal/MediaPreview.tsx | 22 +++--- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index 72f2308ffeda..57a0cf8e47b5 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -8,7 +8,7 @@ import MediaPreview from './MediaPreview'; export enum FilePreviewType { IMAGE = 'image', AUDIO = 'audio', - VIDEO = 'video', + // VIDEO = 'video', // currently showing it in simple generic view } const getFileType = (fileType: File['type']): FilePreviewType | undefined => { @@ -54,7 +54,7 @@ const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement }; if (shouldShowMediaPreview(file, fileType)) { - return ; + return ; } return ; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index 3d07c8d0f3c4..09b75ed27849 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,24 +1,67 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import { formatBytes } from '../../../../lib/utils/formatBytes'; type GenericPreviewProps = { file: File; - index: number; // Add index as a prop - onRemove: (index: number) => void; // Function to handle file removal with index + index: number; + onRemove: (index: number) => void; }; -const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => ( - - - - {`${file.name} - ${formatBytes(file.size, 2)}`} - - - onRemove(index)} /> {/* Pass index to onRemove */} - - -); +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const buttonStyle: React.CSSProperties = { + position: 'absolute' as const, + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + return ( + + + {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( + + ) : ( + + )} + + + {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} + {`${file.name.split('.')[1]}`} + + {/*
*/} + onRemove(index)} /> + + ); +}; export default GenericPreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx index a0b012167671..de15c8444acb 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -8,11 +8,14 @@ import PreviewSkeleton from './PreviewSkeleton'; type ImagePreviewProps = { url: string; file: File; + onRemove: (index: number) => void; + index: number; }; -const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { +const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { const [error, setError] = useState(false); const [loading, setLoading] = useState(true); + const [isHovered, setIsHovered] = useState(false); const handleLoad = (): void => setLoading(false); const handleError = (): void => { @@ -20,23 +23,39 @@ const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { setError(true); }; + const handleMouseEnter = (): void => setIsHovered(true); + const handleMouseLeave = (): void => setIsHovered(false); + + const buttonStyle: React.CSSProperties = { + position: 'absolute', + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + if (error) { - return ; + return ; } return ( - <> + {loading && } - + onRemove(index)} /> + ); }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index 6b48a47a79ef..bee5e911675d 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -34,9 +34,11 @@ const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader[ type MediaPreviewProps = { file: File; fileType: FilePreviewType; + onRemove: (index: number) => void; + index: number; }; -const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { +const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { const [loaded, url] = useFileAsDataURL(file); const t = useTranslation(); @@ -54,17 +56,17 @@ const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { } if (fileType === FilePreviewType.IMAGE) { - return ; + return ; } - if (fileType === FilePreviewType.VIDEO) { - return ( - - - {t('Browser_does_not_support_video_element')} - - ); - } + // if (fileType === FilePreviewType.VIDEO) { + // return ( + // + // + // {t('Browser_does_not_support_video_element')} + // + // ); + // } if (fileType === FilePreviewType.AUDIO) { return ; From 5a9f6466ef8d0a0e86ca071742b5a08b94aced1d Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 00:42:54 +0530 Subject: [PATCH 060/112] merge develop --- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 707e55480e0f..2c145fd16f59 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,7 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Users } from '@rocket.chat/models'; +import { Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; From 771314ee32d959fb9b513aba61b0e81f6c1283d2 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:17:40 +0530 Subject: [PATCH 061/112] feat: Added a file upload preview inside the messageBox --- .../room/composer/messageBox/MessageBox.tsx | 247 +++++++++++++++++- .../MessageBoxActionsToolbar.tsx | 4 +- .../hooks/useFileUploadAction.ts | 4 +- 3 files changed, 245 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index c29a9149d86c..95d47926cd5a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction, @@ -12,10 +12,10 @@ import { MessageComposerHint, MessageComposerButton, } from '@rocket.chat/ui-composer'; -import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts'; +import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; -import React, { memo, useRef, useReducer, useCallback } from 'react'; +import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; @@ -45,6 +45,11 @@ import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; +import FilePreview from '../../modals/FileUploadModal/FilePreview'; +import { Box } from '@rocket.chat/fuselage'; +import fileSize from 'filesize'; +import { e2e } from '/app/e2e/client'; +import { getFileExtension } from '/lib/utils/getFileExtension'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -117,7 +122,60 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); + const [uploadfiles, setUploadfiles] = useState([]); + const dispatchToastMessage = useToastMessageDispatch(); + const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; + function handlefileUpload(fileslist: File[], resetFileInput?: () => void) { + setUploadfiles((prevFiles) => [...prevFiles, ...fileslist]); + resetFileInput?.(); + } + + const handleremove = (index: number) => { + if (!uploadfiles) { + return; + } + const temp = uploadfiles.filter((_, i) => { + return i !== index; + }); + setUploadfiles(temp); + }; + + const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); + }; + const uploadFile = ( + file: File[] | File, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + ) => { + if (!chat) { + console.error('Chat context not found'); + return; + } + const msg = chat.composer?.text ?? ''; + chat.composer?.clear(); + setUploadfiles([]); + chat.uploads.send( + file, + { + msg, + ...extraData, + }, + getContent, + fileContent, + ); + }; const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -158,7 +216,166 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); - const handleSendMessage = useMutableCallback(() => { + const handleSendMessage = useMutableCallback(async () => { + if (uploadfiles !== undefined && uploadfiles.length > 0) { + const msg = chat.composer?.text ?? ''; + if (uploadfiles.length > 6) { + dispatchToastMessage({ + type: 'error', + message: "You can't upload more than 6 files at once", + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + for (const queuedFile of uploadfiles) { + const { name, size } = queuedFile; + if (!name) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + + // Validate file size + if (maxFileSize > -1 && (size || 0) > maxFileSize) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + } + + Object.defineProperty(uploadfiles[0], 'name', { + writable: true, + value: uploadfiles[0].name, + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(uploadfiles, { msg }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(uploadfiles, { msg }); + return; + } + const encryptedFilesarray: any = await Promise.all( + uploadfiles.map(async (file) => { + return await e2eRoom.encryptFile(file); + }), + ); + const filesarray = encryptedFilesarray.map((file: any) => { + return file?.file; + }); + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: uploadfiles[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(uploadfiles[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: uploadfiles[i].type, + image_size: uploadfiles[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: uploadfiles[i].type, + audio_size: uploadfiles[i].size, + }); + } else if (/^video\/.+/.test(uploadfiles[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: uploadfiles[i].type, + video_size: uploadfiles[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: uploadfiles[i].size, + format: getFileExtension(uploadfiles[i].name), + }); + } + + const files = { + _id: _id[i], + name: uploadfiles[i].name, + type: uploadfiles[i].type, + size: uploadfiles[i].size, + // "format": "png" + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: uploadfiles[0], + }); + }; + + const fileContentData = { + type: uploadfiles[0].type, + typeGroup: uploadfiles[0].type.split('/')[0], + name: uploadfiles[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + uploadFile( + filesarray, + { + t: 'e2e', + }, + getContent, + fileContent, + ); + } + chat.composer?.clear(); + return; + } const text = chat.composer?.text ?? ''; chat.composer?.clear(); clearPopup(); @@ -416,6 +633,21 @@ const MessageBox = ({ aria-activedescendant={ariaActiveDescendant} />
+ + {uploadfiles !== undefined && uploadfiles.length > 0 && ( + <> + {uploadfiles.map((file, index) => ( + + ))} + + )} + @@ -455,10 +688,10 @@ const MessageBox = ({ 0))} onClick={handleSendMessage} - secondary={typing || isEditing} - info={typing || isEditing} + secondary={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} + info={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} /> )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 90216f159069..69dbd4655a9c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,6 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; + handlefiles?: any; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -45,6 +46,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + handlefiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -57,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handlefiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index f911b2b63b1f..888d7b765a79 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -7,7 +7,7 @@ import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean, handlefiles?: any): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); @@ -30,7 +30,7 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => }); return file; }); - chat?.flows.uploadFiles(filesToUpload, resetFileInput); + handlefiles(filesToUpload, resetFileInput); }; fileInputRef.current?.addEventListener('change', handleUploadChange); From 755c08b07bb856aac9b8c967bd466c947f4543b7 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:46:37 +0530 Subject: [PATCH 062/112] Fix issue in uploads.ts causing duplicate messages on send --- apps/meteor/client/lib/chats/uploads.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index fac41d6bbc09..9a51487326f2 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -128,7 +128,6 @@ const send = async ( } const text: IMessage = { rid, - msg, _id: id, }; try { From f2bd5523a9dff4109e6f56f80ea00d3984abb50b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:53:20 +0530 Subject: [PATCH 063/112] Cleanup: Remove unnecessary code --- apps/meteor/app/api/server/v1/rooms.ts | 1 - ee/packages/api-client/src/index.ts | 14 +++----------- packages/rest-typings/src/v1/rooms.ts | 3 +-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index fc4175c996ac..e3296b98ef17 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -140,7 +140,6 @@ API.v1.addRoute( }, ); -// TODO: deprecate API API.v1.addRoute( 'rooms.upload/:rid', { diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index 9a16f8d1efde..a11e032e91b2 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -260,19 +260,11 @@ export class RestClient implements RestClientInterface { const data = new FormData(); Object.entries(params as any).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((file) => { - if (file instanceof File) { - data.append(key, file, file.name); - return; - } - file && data.append(key, file as any); - }); - } else if (value instanceof File) { + if (value instanceof File) { data.append(key, value, value.name); - } else { - value && data.append(key, value as any); + return; } + value && data.append(key, value as any); }); xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 68d6305fb812..1c0b6a360f7b 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -622,7 +622,7 @@ export type RoomsEndpoints = { }; '/v1/rooms.media/:rid': { - POST: (params: { file: File | File[] }) => { file: { url: string } }; + POST: (params: { file: File }) => { file: { url: string } }; }; '/v1/rooms.mediaConfirm/:rid/:fileId': { @@ -637,7 +637,6 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; content?: IE2EEMessage['content']; - fileIds?: string[]; }) => { message: IMessage | null }; }; From 39ff2a3fb13cac185db88fdea2a14271fee2f327 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 23:11:16 +0530 Subject: [PATCH 064/112] removed unused space --- apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 95d47926cd5a..e4e7bb98a7b4 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -620,7 +620,6 @@ const MessageBox = ({ {isRecordingVideo && } {isRecordingAudio && } - Date: Thu, 8 Aug 2024 09:18:16 +0530 Subject: [PATCH 065/112] fix: lint errors --- .../app/file-upload/server/methods/sendFileMessage.ts | 2 +- apps/meteor/app/lib/server/methods/sendMessage.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 05f92bc6de65..5b0904e5afa6 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -16,7 +16,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; -import { SystemLogger } from '../../../../server/lib/logger/system'; +// import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 2c145fd16f59..41887ad36c0e 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -15,10 +15,10 @@ import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -import { canAccessRoomIdAsync } from '/app/authorization/server/functions/canAccessRoom'; -import { API } from '/app/api/server'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { API } from '../../../../app/api/server/api'; import { IUpload } from '@rocket.chat/core-typings'; -import { sendFileMessage } from '/app/file-upload/server/methods/sendFileMessage'; +import { sendFileMessage } from '../../../../app/file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { From f4b60d5af443d6c7ed281160c4154090b1618d44 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 11:43:38 +0530 Subject: [PATCH 066/112] fix: lint and TS errors --- .../server/methods/sendFileMessage.ts | 5 +++-- .../client/lib/chats/flows/uploadFiles.ts | 9 +++++--- apps/meteor/client/lib/chats/uploads.ts | 10 ++++++++- .../room/composer/messageBox/MessageBox.tsx | 14 ++++++++----- .../FileUploadModal/FileUploadModal.tsx | 21 +++++-------------- .../modals/FileUploadModal/MediaPreview.tsx | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5b0904e5afa6..70f70bb22f65 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -32,7 +32,7 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL export const parseFileIntoMessageAttachments = async ( filearr: Partial[] | Partial, - roomId: string, + // roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; @@ -219,7 +219,8 @@ export const sendFileMessage = async ( }; if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + // const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, user); data.file = files[0]; data.files = files; data.attachments = attachments; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index cade8623b30e..4e7ebebac63b 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -40,7 +40,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File[] | File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { @@ -159,7 +159,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi format: getFileExtension(queue[i].name), }); } - const files = { _id: _id[i], @@ -194,13 +193,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; uploadFile( filesarray, { t: 'e2e', }, getContent, - fileContent, + uploadFileData, ); } }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 9a51487326f2..de17f7050d1d 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -55,7 +55,7 @@ const send = async ( ...uploads, { id, - name: file[0].name || fileContent?.raw.name || file[0]?.file?.name, + name: file[0].name || fileContent?.raw.name || 'unknown', percentage: 0, }, ]); @@ -126,9 +126,17 @@ const send = async ( if (msg == undefined) { msg = ''; } + // const text: IMessage = { + // rid, + // _id: id, + // }; const text: IMessage = { rid, _id: id, + msg: '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), }; try { let content; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index e4e7bb98a7b4..06300c4bcbf5 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -48,8 +48,9 @@ import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; import FilePreview from '../../modals/FileUploadModal/FilePreview'; import { Box } from '@rocket.chat/fuselage'; import fileSize from 'filesize'; -import { e2e } from '/app/e2e/client'; -import { getFileExtension } from '/lib/utils/getFileExtension'; +// import { e2e } from '/app/e2e/client'; +import { e2e } from '../../../../../app/e2e/client'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -155,7 +156,7 @@ const MessageBox = ({ }; const uploadFile = ( file: File[] | File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { @@ -240,7 +241,6 @@ const MessageBox = ({ return; } - // Validate file size if (maxFileSize > -1 && (size || 0) > maxFileSize) { dispatchToastMessage({ type: 'error', @@ -364,13 +364,17 @@ const MessageBox = ({ const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; uploadFile( filesarray, { t: 'e2e', }, getContent, - fileContent, + uploadFileData, ); } chat.composer?.clear(); diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index bb8175a975ae..c32a59772f41 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,17 +1,4 @@ -import { - Modal, - Box, - Field, - FieldGroup, - FieldLabel, - FieldRow, - FieldError, - TextInput, - Button, - Scrollable, - Tile, - Icon, -} from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, TextInput, Button, Scrollable, Tile, Icon } from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -25,7 +12,7 @@ type FileUploadModalProps = { queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; - updateQueue: (queue: File[]) => void; + updateQueue?: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -71,7 +58,9 @@ const FileUploadModal = ({ const target = e.target as HTMLInputElement; const files = Array.from(target.files as FileList); setQueue1([...queue1, ...files]); - updateQueue([...queue1, ...files]); + if (updateQueue !== undefined) { + updateQueue([...queue1, ...files]); + } }; }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index bee5e911675d..a5eed5a03db3 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useState, memo } from 'react'; -import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; +// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view import { FilePreviewType } from './FilePreview'; import ImagePreview from './ImagePreview'; import PreviewSkeleton from './PreviewSkeleton'; From 1c0dddeb6f73b623955d3901ef8f1a064d724d78 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 12:02:39 +0530 Subject: [PATCH 067/112] fix: ensure file handling as array to resolve type errors in uploads --- apps/meteor/client/lib/chats/uploads.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index de17f7050d1d..47637e2e5893 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -47,23 +47,21 @@ const send = async ( getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { - if (!Array.isArray(file)) { - file = [file]; - } + const files = Array.isArray(file) ? file : [file]; const id = Random.id(); updateUploads((uploads) => [ ...uploads, { id, - name: file[0].name || fileContent?.raw.name || 'unknown', + name: files[0].name || fileContent?.raw.name || 'unknown', percentage: 0, }, ]); - var fileIds: string[] = []; - var fileUrls: string[] = []; + const fileIds: string[] = []; + const fileUrls: string[] = []; - file.map((f) => { + files.forEach((f) => { new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, @@ -74,7 +72,6 @@ const send = async ( }), }, { - // load: () => resolve(), progress: (event) => { if (!event.lengthComputable) { return; @@ -82,7 +79,6 @@ const send = async ( const progress = (event.loaded / event.total) * 100; if (progress === 100) { resolve(); - // return; } updateUploads((uploads) => @@ -122,14 +118,11 @@ const send = async ( const result = JSON.parse(xhr.responseText); fileIds.push(result.file._id); fileUrls.push(result.file.url); - if (fileIds.length === file.length) { + if (fileIds.length === files.length) { if (msg == undefined) { msg = ''; } - // const text: IMessage = { - // rid, - // _id: id, - // }; + const text: IMessage = { rid, _id: id, @@ -138,6 +131,7 @@ const send = async ( u: { _id: id, username: id }, _updatedAt: new Date(), }; + try { let content; if (getContent) { From f5213b0f46dbc55e9808351a9f3f0f3fc7beefff Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 19:06:22 +0530 Subject: [PATCH 068/112] fix: lint error and TS errors --- .../server/methods/sendFileMessage.ts | 5 ++--- .../app/lib/server/methods/sendMessage.ts | 21 +++++++++---------- .../room/composer/messageBox/MessageBox.tsx | 5 ++--- .../modals/FileUploadModal/GenericPreview.tsx | 1 + 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 70f70bb22f65..5b0904e5afa6 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -32,7 +32,7 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL export const parseFileIntoMessageAttachments = async ( filearr: Partial[] | Partial, - // roomId: string, + roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; @@ -219,8 +219,7 @@ export const sendFileMessage = async ( }; if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { - // const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - const { files, attachments } = await parseFileIntoMessageAttachments(file, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; data.files = files; data.attachments = attachments; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 41887ad36c0e..b164e86674d8 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, IUpload } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; @@ -16,9 +16,8 @@ import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { API } from '../../../../app/api/server/api'; -import { IUpload } from '@rocket.chat/core-typings'; -import { sendFileMessage } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { API } from '../../../api/server/api'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -96,7 +95,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast({ throw new Error("Cannot send system messages using 'sendMessage'"); } - if (filesToConfirm != undefined) { + if (filesToConfirm !== undefined) { if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } const filesarray: Partial[] = []; - for (let i = 0; i < filesToConfirm.length; i++) { - const fileid = filesToConfirm[i]; + for (const fileid of filesToConfirm) { const file = await Uploads.findOneById(fileid); if (!file) { throw new Meteor.Error('invalid-file'); @@ -152,11 +150,11 @@ Meteor.methods({ } await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); - for (let i = 0; i < filesToConfirm.length; i++) { - await Uploads.confirmTemporaryFile(filesToConfirm[i], uid); + for (const fileid of filesToConfirm) { + await Uploads.confirmTemporaryFile(fileid, uid); } let resmessage; - if (filesarray[0] != null && filesarray[0]._id != undefined) { + if (filesarray[0] !== null && filesarray[0]._id !== undefined) { resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); } @@ -176,6 +174,7 @@ Meteor.methods({ } }, }); + // Limit a user, who does not have the "bot" role, to sending 5 msgs/second RateLimiter.limitMethod('sendMessage', 5, 1000, { async userId(userId: IUser['_id']) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 06300c4bcbf5..f21d69347b63 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -45,12 +45,11 @@ import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; -import FilePreview from '../../modals/FileUploadModal/FilePreview'; -import { Box } from '@rocket.chat/fuselage'; import fileSize from 'filesize'; -// import { e2e } from '/app/e2e/client'; import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; +import { Box } from '@rocket.chat/fuselage'; +import FilePreview from '../../modals/FileUploadModal/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index 09b75ed27849..b7d2367fc643 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,6 +1,7 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; + import { formatBytes } from '../../../../lib/utils/formatBytes'; type GenericPreviewProps = { From 6f398b4ad830b06016a9739439a121c6b43949c8 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 21:53:54 +0530 Subject: [PATCH 069/112] fix: lint and TS errors --- .../app/lib/server/methods/sendMessage.ts | 31 +-- .../client/lib/chats/flows/uploadFiles.ts | 18 +- apps/meteor/client/lib/chats/uploads.ts | 2 +- .../room/composer/messageBox/MessageBox.tsx | 188 +++++++++++++++++- 4 files changed, 200 insertions(+), 39 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index b164e86674d8..31d3129b0c58 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -8,16 +8,16 @@ import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { API } from '../../../api/server/api'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; +import { metrics } from '../../../metrics/server'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { API } from '../../../api/server/api'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -140,19 +140,20 @@ Meteor.methods({ if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = []; - for (const fileid of filesToConfirm) { - const file = await Uploads.findOneById(fileid); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - filesarray.push(file); - } + const filesarray: Partial[] = await Promise.all( + filesToConfirm.map(async (fileid) => { + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + return file; + }), + ); + await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); - for (const fileid of filesToConfirm) { - await Uploads.confirmTemporaryFile(fileid, uid); - } + await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); + let resmessage; if (filesarray[0] !== null && filesarray[0]._id !== undefined) { resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 4e7ebebac63b..2631f7e44641 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -75,7 +75,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi onClose: (): void => { imperativeModal.close(); }, - // onSubmit: async (fileName: string, description?: string): Promise => { onSubmit: async (fileName: string, msg?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, @@ -95,18 +94,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi uploadFile(queue, { msg }); return; } - console.log('uploading file', file); - console.log('message ', msg); - const encryptedFilesarray: any = await Promise.all( - queue.map(async (file) => { - return await e2eRoom.encryptFile(file); - }), - ); - console.log('encryptedFilesarray', encryptedFilesarray); - const filesarray = encryptedFilesarray.map((file: any) => { - return file?.file; - }); - console.log('filesarray', filesarray); + + const encryptedFilesarray: any = await Promise.all(queue.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -165,7 +156,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi name: queue[i].name, type: queue[i].type, size: queue[i].size, - // "format": "png" }; arrayoffiles.push(files); } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 47637e2e5893..d1116d580af4 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -119,7 +119,7 @@ const send = async ( fileIds.push(result.file._id); fileUrls.push(result.file.url); if (fileIds.length === files.length) { - if (msg == undefined) { + if (msg === undefined) { msg = ''; } diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f21d69347b63..4bebc068e215 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -269,14 +269,10 @@ const MessageBox = ({ uploadFile(uploadfiles, { msg }); return; } - const encryptedFilesarray: any = await Promise.all( - uploadfiles.map(async (file) => { - return await e2eRoom.encryptFile(file); - }), - ); - const filesarray = encryptedFilesarray.map((file: any) => { - return file?.file; - }); + + const encryptedFilesarray: any = await Promise.all(uploadfiles.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -335,7 +331,6 @@ const MessageBox = ({ name: uploadfiles[i].name, type: uploadfiles[i].type, size: uploadfiles[i].size, - // "format": "png" }; arrayoffiles.push(files); } @@ -391,6 +386,181 @@ const MessageBox = ({ }); }); + // const handleSendMessage = useMutableCallback(async () => { + // if (uploadfiles !== undefined && uploadfiles.length > 0) { + // const msg = chat.composer?.text ?? ''; + // if (uploadfiles.length > 6) { + // dispatchToastMessage({ + // type: 'error', + // message: "You can't upload more than 6 files at once", + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + // for (const queuedFile of uploadfiles) { + // const { name, size } = queuedFile; + // if (!name) { + // dispatchToastMessage({ + // type: 'error', + // message: t('error-the-field-is-required', { field: t('Name') }), + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + + // if (maxFileSize > -1 && (size || 0) > maxFileSize) { + // dispatchToastMessage({ + // type: 'error', + // message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + // } + + // Object.defineProperty(uploadfiles[0], 'name', { + // writable: true, + // value: uploadfiles[0].name, + // }); + + // const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + // if (!e2eRoom) { + // uploadFile(uploadfiles, { msg }); + // return; + // } + + // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + // if (!shouldConvertSentMessages) { + // uploadFile(uploadfiles, { msg }); + // return; + // } + // const encryptedFilesarray: any = await Promise.all( + // uploadfiles.map(async (file) => { + // return await e2eRoom.encryptFile(file); + // }), + // ); + // const filesarray = encryptedFilesarray.map((file: any) => { + // return file?.file; + // }); + // if (encryptedFilesarray[0]) { + // const getContent = async (_id: string[], fileUrl: string[]): Promise => { + // const attachments = []; + // const arrayoffiles = []; + // for (let i = 0; i < _id.length; i++) { + // const attachment: FileAttachmentProps = { + // title: uploadfiles[i].name, + // type: 'file', + // title_link: fileUrl[i], + // title_link_download: true, + // encryption: { + // key: encryptedFilesarray[i].key, + // iv: encryptedFilesarray[i].iv, + // }, + // hashes: { + // sha256: encryptedFilesarray[i].hash, + // }, + // }; + + // if (/^image\/.+/.test(uploadfiles[i].type)) { + // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + + // attachments.push({ + // ...attachment, + // image_url: fileUrl[i], + // image_type: uploadfiles[i].type, + // image_size: uploadfiles[i].size, + // ...(dimensions && { + // image_dimensions: dimensions, + // }), + // }); + // } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + // attachments.push({ + // ...attachment, + // audio_url: fileUrl[i], + // audio_type: uploadfiles[i].type, + // audio_size: uploadfiles[i].size, + // }); + // } else if (/^video\/.+/.test(uploadfiles[i].type)) { + // attachments.push({ + // ...attachment, + // video_url: fileUrl[i], + // video_type: uploadfiles[i].type, + // video_size: uploadfiles[i].size, + // }); + // } else { + // attachments.push({ + // ...attachment, + // size: uploadfiles[i].size, + // format: getFileExtension(uploadfiles[i].name), + // }); + // } + + // const files = { + // _id: _id[i], + // name: uploadfiles[i].name, + // type: uploadfiles[i].type, + // size: uploadfiles[i].size, + // // "format": "png" + // }; + // arrayoffiles.push(files); + // } + + // return e2eRoom.encryptMessageContent({ + // attachments, + // files: arrayoffiles, + // file: uploadfiles[0], + // }); + // }; + + // const fileContentData = { + // type: uploadfiles[0].type, + // typeGroup: uploadfiles[0].type.split('/')[0], + // name: uploadfiles[0].name, + // msg: msg || '', + // encryption: { + // key: encryptedFilesarray[0].key, + // iv: encryptedFilesarray[0].iv, + // }, + // hashes: { + // sha256: encryptedFilesarray[0].hash, + // }, + // }; + + // const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + // const uploadFileData = { + // raw: {}, + // encrypted: fileContent, + // }; + // uploadFile( + // filesarray, + // { + // t: 'e2e', + // }, + // getContent, + // uploadFileData, + // ); + // } + // chat.composer?.clear(); + // return; + // } + // const text = chat.composer?.text ?? ''; + // chat.composer?.clear(); + // clearPopup(); + + // onSend?.({ + // value: text, + // tshow, + // previewUrls, + // isSlashCommandAllowed, + // }); + // }); + const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); From cdc0ff5ff9f82be59af8bdf4b4e70e478ebf129d Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 21:56:23 +0530 Subject: [PATCH 070/112] removed unused code --- .../room/composer/messageBox/MessageBox.tsx | 176 ------------------ 1 file changed, 176 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 4bebc068e215..bf1ccb3a57f0 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -385,182 +385,6 @@ const MessageBox = ({ isSlashCommandAllowed, }); }); - - // const handleSendMessage = useMutableCallback(async () => { - // if (uploadfiles !== undefined && uploadfiles.length > 0) { - // const msg = chat.composer?.text ?? ''; - // if (uploadfiles.length > 6) { - // dispatchToastMessage({ - // type: 'error', - // message: "You can't upload more than 6 files at once", - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - // for (const queuedFile of uploadfiles) { - // const { name, size } = queuedFile; - // if (!name) { - // dispatchToastMessage({ - // type: 'error', - // message: t('error-the-field-is-required', { field: t('Name') }), - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - - // if (maxFileSize > -1 && (size || 0) > maxFileSize) { - // dispatchToastMessage({ - // type: 'error', - // message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - // } - - // Object.defineProperty(uploadfiles[0], 'name', { - // writable: true, - // value: uploadfiles[0].name, - // }); - - // const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - // if (!e2eRoom) { - // uploadFile(uploadfiles, { msg }); - // return; - // } - - // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - // if (!shouldConvertSentMessages) { - // uploadFile(uploadfiles, { msg }); - // return; - // } - // const encryptedFilesarray: any = await Promise.all( - // uploadfiles.map(async (file) => { - // return await e2eRoom.encryptFile(file); - // }), - // ); - // const filesarray = encryptedFilesarray.map((file: any) => { - // return file?.file; - // }); - // if (encryptedFilesarray[0]) { - // const getContent = async (_id: string[], fileUrl: string[]): Promise => { - // const attachments = []; - // const arrayoffiles = []; - // for (let i = 0; i < _id.length; i++) { - // const attachment: FileAttachmentProps = { - // title: uploadfiles[i].name, - // type: 'file', - // title_link: fileUrl[i], - // title_link_download: true, - // encryption: { - // key: encryptedFilesarray[i].key, - // iv: encryptedFilesarray[i].iv, - // }, - // hashes: { - // sha256: encryptedFilesarray[i].hash, - // }, - // }; - - // if (/^image\/.+/.test(uploadfiles[i].type)) { - // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); - - // attachments.push({ - // ...attachment, - // image_url: fileUrl[i], - // image_type: uploadfiles[i].type, - // image_size: uploadfiles[i].size, - // ...(dimensions && { - // image_dimensions: dimensions, - // }), - // }); - // } else if (/^audio\/.+/.test(uploadfiles[i].type)) { - // attachments.push({ - // ...attachment, - // audio_url: fileUrl[i], - // audio_type: uploadfiles[i].type, - // audio_size: uploadfiles[i].size, - // }); - // } else if (/^video\/.+/.test(uploadfiles[i].type)) { - // attachments.push({ - // ...attachment, - // video_url: fileUrl[i], - // video_type: uploadfiles[i].type, - // video_size: uploadfiles[i].size, - // }); - // } else { - // attachments.push({ - // ...attachment, - // size: uploadfiles[i].size, - // format: getFileExtension(uploadfiles[i].name), - // }); - // } - - // const files = { - // _id: _id[i], - // name: uploadfiles[i].name, - // type: uploadfiles[i].type, - // size: uploadfiles[i].size, - // // "format": "png" - // }; - // arrayoffiles.push(files); - // } - - // return e2eRoom.encryptMessageContent({ - // attachments, - // files: arrayoffiles, - // file: uploadfiles[0], - // }); - // }; - - // const fileContentData = { - // type: uploadfiles[0].type, - // typeGroup: uploadfiles[0].type.split('/')[0], - // name: uploadfiles[0].name, - // msg: msg || '', - // encryption: { - // key: encryptedFilesarray[0].key, - // iv: encryptedFilesarray[0].iv, - // }, - // hashes: { - // sha256: encryptedFilesarray[0].hash, - // }, - // }; - - // const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - // const uploadFileData = { - // raw: {}, - // encrypted: fileContent, - // }; - // uploadFile( - // filesarray, - // { - // t: 'e2e', - // }, - // getContent, - // uploadFileData, - // ); - // } - // chat.composer?.clear(); - // return; - // } - // const text = chat.composer?.text ?? ''; - // chat.composer?.clear(); - // clearPopup(); - - // onSend?.({ - // value: text, - // tshow, - // previewUrls, - // isSlashCommandAllowed, - // }); - // }); - const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); From a1faa22add2d59fede70d82ab52ddf855deeb509 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 23:23:01 +0530 Subject: [PATCH 071/112] fix: reorder imports to fix ESLint errors and removed unused changes --- apps/meteor/app/lib/server/methods/sendMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 31d3129b0c58..6aa937951f11 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -8,12 +8,12 @@ import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { API } from '../../../api/server/api'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; -import { metrics } from '../../../metrics/server'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; +import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; @@ -95,7 +95,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast Date: Thu, 8 Aug 2024 23:35:33 +0530 Subject: [PATCH 072/112] fix: changed the variable name with camelCase and removed unwanted chages --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 - .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 6 +++--- .../MessageBoxActionsToolbar/hooks/useFileUploadAction.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 6aa937951f11..572615e71196 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -175,7 +175,6 @@ Meteor.methods({ } }, }); - // Limit a user, who does not have the "bot" role, to sending 5 msgs/second RateLimiter.limitMethod('sendMessage', 5, 1000, { async userId(userId: IUser['_id']) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 69dbd4655a9c..bf6f61c77bad 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,7 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - handlefiles?: any; + handleFiles?: any; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -46,7 +46,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, - handlefiles, + handleFiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -59,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handlefiles); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 888d7b765a79..304a0973ddc6 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -7,7 +7,7 @@ import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean, handlefiles?: any): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean, handleFiles?: any): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); @@ -30,7 +30,7 @@ export const useFileUploadAction = (disabled: boolean, handlefiles?: any): Gener }); return file; }); - handlefiles(filesToUpload, resetFileInput); + handleFiles(filesToUpload, resetFileInput); }; fileInputRef.current?.addEventListener('change', handleUploadChange); From c07808308039e18c8fce323dd7375e99d515f2d9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 9 Aug 2024 02:53:32 +0530 Subject: [PATCH 073/112] fix: changed variable names to cameCase --- .../room/composer/messageBox/MessageBox.tsx | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index bf1ccb3a57f0..a414f655757b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -122,23 +122,19 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); - const [uploadfiles, setUploadfiles] = useState([]); + const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handlefileUpload(fileslist: File[], resetFileInput?: () => void) { - setUploadfiles((prevFiles) => [...prevFiles, ...fileslist]); + function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { + setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); resetFileInput?.(); } - const handleremove = (index: number) => { - if (!uploadfiles) { - return; - } - const temp = uploadfiles.filter((_, i) => { - return i !== index; - }); - setUploadfiles(temp); + const handleRemoveFile = (index: number) => { + const temp = [...filesToUpload]; + temp.splice(index, 1); + setFilesToUpload(temp); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -165,7 +161,7 @@ const MessageBox = ({ } const msg = chat.composer?.text ?? ''; chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); chat.uploads.send( file, { @@ -217,18 +213,18 @@ const MessageBox = ({ }); const handleSendMessage = useMutableCallback(async () => { - if (uploadfiles !== undefined && uploadfiles.length > 0) { + if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - if (uploadfiles.length > 6) { + if (filesToUpload.length > 6) { dispatchToastMessage({ type: 'error', message: "You can't upload more than 6 files at once", }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } - for (const queuedFile of uploadfiles) { + for (const queuedFile of filesToUpload) { const { name, size } = queuedFile; if (!name) { dispatchToastMessage({ @@ -236,7 +232,7 @@ const MessageBox = ({ message: t('error-the-field-is-required', { field: t('Name') }), }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } @@ -246,31 +242,31 @@ const MessageBox = ({ message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } } - Object.defineProperty(uploadfiles[0], 'name', { + Object.defineProperty(filesToUpload[0], 'name', { writable: true, - value: uploadfiles[0].name, + value: filesToUpload[0].name, }); const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(uploadfiles, { msg }); + uploadFile(filesToUpload, { msg }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(uploadfiles, { msg }); + uploadFile(filesToUpload, { msg }); return; } - const encryptedFilesarray: any = await Promise.all(uploadfiles.map((file) => e2eRoom.encryptFile(file))); + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); const filesarray = encryptedFilesarray.map((file: any) => file?.file); if (encryptedFilesarray[0]) { @@ -279,7 +275,7 @@ const MessageBox = ({ const arrayoffiles = []; for (let i = 0; i < _id.length; i++) { const attachment: FileAttachmentProps = { - title: uploadfiles[i].name, + title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], title_link_download: true, @@ -292,45 +288,45 @@ const MessageBox = ({ }, }; - if (/^image\/.+/.test(uploadfiles[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); attachments.push({ ...attachment, image_url: fileUrl[i], - image_type: uploadfiles[i].type, - image_size: uploadfiles[i].size, + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, ...(dimensions && { image_dimensions: dimensions, }), }); - } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { attachments.push({ ...attachment, audio_url: fileUrl[i], - audio_type: uploadfiles[i].type, - audio_size: uploadfiles[i].size, + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, }); - } else if (/^video\/.+/.test(uploadfiles[i].type)) { + } else if (/^video\/.+/.test(filesToUpload[i].type)) { attachments.push({ ...attachment, video_url: fileUrl[i], - video_type: uploadfiles[i].type, - video_size: uploadfiles[i].size, + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, }); } else { attachments.push({ ...attachment, - size: uploadfiles[i].size, - format: getFileExtension(uploadfiles[i].name), + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), }); } const files = { _id: _id[i], - name: uploadfiles[i].name, - type: uploadfiles[i].type, - size: uploadfiles[i].size, + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, }; arrayoffiles.push(files); } @@ -338,14 +334,14 @@ const MessageBox = ({ return e2eRoom.encryptMessageContent({ attachments, files: arrayoffiles, - file: uploadfiles[0], + file: filesToUpload[0], }); }; const fileContentData = { - type: uploadfiles[0].type, - typeGroup: uploadfiles[0].type.split('/')[0], - name: uploadfiles[0].name, + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, msg: msg || '', encryption: { key: encryptedFilesarray[0].key, @@ -636,10 +632,10 @@ const MessageBox = ({ width: '100%', }} > - {uploadfiles !== undefined && uploadfiles.length > 0 && ( + {filesToUpload.length > 0 && ( <> - {uploadfiles.map((file, index) => ( - + {filesToUpload.map((file, index) => ( + ))} )} @@ -669,7 +665,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} - handlefiles={handlefileUpload} + handleFiles={handleFileUpload} /> @@ -684,10 +680,10 @@ const MessageBox = ({ 0))} + disabled={!canSend || (!typing && !isEditing && !(filesToUpload.length > 0))} onClick={handleSendMessage} - secondary={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} - info={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} + secondary={typing || isEditing || filesToUpload.length > 0} + info={typing || isEditing || filesToUpload.length > 0} /> )} From 0e4d827f05be4e0af9fe14618a0fb53678960b20 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 9 Aug 2024 18:58:47 +0530 Subject: [PATCH 074/112] fix: added toast message while uploading file --- .../room/composer/messageBox/MessageBox.tsx | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index a414f655757b..8970aa825142 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -125,12 +125,53 @@ const MessageBox = ({ const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { - setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); + + function handleFileUpload(filesList: File[], resetFileInput?: () => void) { + setFilesToUpload((prevFiles) => { + let newFilesToUpload = [...prevFiles, ...filesList]; + + if (newFilesToUpload.length > 6) { + newFilesToUpload = newFilesToUpload.slice(0, 6); + dispatchToastMessage({ + type: 'error', + message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", + }); + } + + const validFiles = newFilesToUpload.filter((queuedFile) => { + const { name, size } = queuedFile; + + if (!name) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + return false; + } + + if (maxFileSize > -1 && (size || 0) > maxFileSize) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + return false; + } + + return true; + }); + + return validFiles; + }); resetFileInput?.(); } + // function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { + // setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); + + // resetFileInput?.(); + // } + const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; temp.splice(index, 1); @@ -215,37 +256,6 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - if (filesToUpload.length > 6) { - dispatchToastMessage({ - type: 'error', - message: "You can't upload more than 6 files at once", - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - for (const queuedFile of filesToUpload) { - const { name, size } = queuedFile; - if (!name) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - - if (maxFileSize > -1 && (size || 0) > maxFileSize) { - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - } Object.defineProperty(filesToUpload[0], 'name', { writable: true, From 60a80f992f1e04458b5efc6053cce721e7dc4fa0 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 19:20:06 +0530 Subject: [PATCH 075/112] Added the files to upload in the sendMessage in the executeSendMessage --- .../app/lib/server/functions/sendMessage.ts | 24 +++++++++++-- .../app/lib/server/methods/sendMessage.ts | 36 ++++++++++++------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 4a5b8313ebcd..28393b64bc06 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -13,6 +13,7 @@ import { settings } from '../../../settings/server'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; +import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -215,11 +216,30 @@ export function prepareMessageObject( /** * Validates and sends the message object. */ -export const sendMessage = async function (user: any, message: any, room: any, upsert = false, previewUrls?: string[]) { +export const sendMessage = async function ( + user: any, + message: any, + room: any, + upsert = false, + previewUrls?: string[], + filesArray?: Partial[] | Partial, +) { if (!user || !message || !room._id) { return false; } + if (filesArray !== undefined && (typeof filesArray !== undefined || message?.t !== 'e2e')) { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments( + Array.isArray(filesArray) ? filesArray : [filesArray], + roomId, + user, + ); + message.file = files[0]; + message.files = files; + message.attachments = attachments; + } + await validateMessage(message, room, user); prepareMessageObject(message, room._id, user); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 572615e71196..a575f5c27caa 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -19,7 +19,12 @@ import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { +export async function executeSendMessage( + uid: IUser['_id'], + message: AtLeast, + previewUrls?: string[], + filesArray?: Partial[], +) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { method: 'sendMessage', @@ -96,7 +101,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } + let filesArray: Partial[] = []; if (filesToConfirm !== undefined) { if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = await Promise.all( + filesArray = await Promise.all( filesToConfirm.map(async (fileid) => { const file = await Uploads.findOneById(fileid); if (!file) { @@ -149,23 +155,27 @@ Meteor.methods({ return file; }), ); - - await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); + message.msg = msgData?.msg; + message.tmid = msgData?.tmid; + // description, + message.t = msgData?.t; + message.content = msgData?.content; + // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); - let resmessage; - if (filesarray[0] !== null && filesarray[0]._id !== undefined) { - resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); - } + // let resmessage; + // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { + // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); + // } - return API.v1.success({ - resmessage, - }); + // return API.v1.success({ + // resmessage, + // }); } try { - return await executeSendMessage(uid, message, previewUrls); + return await executeSendMessage(uid, message, previewUrls, filesArray); } catch (error: any) { if ((error.error || error.message) === 'error-not-allowed') { throw new Meteor.Error(error.error || error.message, error.reason, { From 7ac5841cafa7a19a89c962f1f671c1329f7bc6e1 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 19:28:32 +0530 Subject: [PATCH 076/112] fix: Revert back as using message API --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5b0904e5afa6..8398cdd86e90 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -163,7 +163,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial[] | Partial; + file: Partial; msgData?: Record; }, { @@ -189,6 +189,7 @@ export const sendFileMessage = async ( if (user?.type !== 'app' && !(await canAccessRoomAsync(room, user))) { return false; } + check( msgData, Match.Maybe({ From 9f7421944bed4ac3dd8e61e0e4d8d142b6680da3 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 20:19:48 +0530 Subject: [PATCH 077/112] Removed unused import --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index a575f5c27caa..e172216edb2d 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -12,7 +12,6 @@ import { API } from '../../../api/server/api'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; From 9afeee679a2a88f9744c1d06da3ca86a3d750d1b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 11 Aug 2024 00:13:55 +0530 Subject: [PATCH 078/112] Remove unwanted code --- .../client/views/room/composer/messageBox/MessageBox.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 8970aa825142..38e5e39a3c33 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -166,12 +166,6 @@ const MessageBox = ({ resetFileInput?.(); } - // function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { - // setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); - - // resetFileInput?.(); - // } - const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; temp.splice(index, 1); From 97f743b4c9d38e4c925dd1ff56ceaaecfad4ec94 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 12 Aug 2024 11:47:02 +0530 Subject: [PATCH 079/112] Added defineProperty for all the files selected --- .../client/views/room/composer/messageBox/MessageBox.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 38e5e39a3c33..ac3048567b26 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -251,9 +251,11 @@ const MessageBox = ({ if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - Object.defineProperty(filesToUpload[0], 'name', { - writable: true, - value: filesToUpload[0].name, + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); }); const e2eRoom = await e2e.getInstanceByRoomId(room._id); From 68f45dcbf427c1038d44074004c8a9694f4ef10a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 13 Aug 2024 15:43:43 +0530 Subject: [PATCH 080/112] Added different function for files and encrypted files sharing --- .../room/composer/messageBox/MessageBox.tsx | 275 +++++++++--------- 1 file changed, 142 insertions(+), 133 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index ac3048567b26..ec0291795254 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -126,10 +126,9 @@ const MessageBox = ({ const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFileUpload(filesList: File[], resetFileInput?: () => void) { + function handleFilesToUpload(filesList: File[], resetFileInput?: () => void) { setFilesToUpload((prevFiles) => { let newFilesToUpload = [...prevFiles, ...filesList]; - if (newFilesToUpload.length > 6) { newFilesToUpload = newFilesToUpload.slice(0, 6); dispatchToastMessage({ @@ -194,19 +193,154 @@ const MessageBox = ({ console.error('Chat context not found'); return; } - const msg = chat.composer?.text ?? ''; - chat.composer?.clear(); - setFilesToUpload([]); chat.uploads.send( file, { - msg, ...extraData, }, getContent, fileContent, ); + chat.composer?.clear(); + setFilesToUpload([]); + }; + + const handleEncryptedFilesShared = async (filesToUpload: File[], msg: string, e2eRoom: any) => { + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: filesToUpload[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, + }); + } else if (/^video\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), + }); + } + + const files = { + _id: _id[i], + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: filesToUpload[0], + }); + }; + + const fileContentData = { + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; + uploadFile( + filesarray, + { + t: 'e2e', + }, + getContent, + uploadFileData, + ); + } }; + const handleSendFiles = async (filesToUpload: File[]) => { + if (!chat || !room) { + return; + } + + const msg = chat.composer?.text ?? ''; + + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(filesToUpload, { msg }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(filesToUpload, { msg }); + return; + } + handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); + chat.composer?.clear(); + setFilesToUpload([]); + return; + }; + const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -249,132 +383,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (filesToUpload.length > 0) { - const msg = chat.composer?.text ?? ''; - - filesToUpload.forEach((file) => { - Object.defineProperty(file, 'name', { - writable: true, - value: file.name, - }); - }); - - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(filesToUpload, { msg }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(filesToUpload, { msg }); - return; - } - - const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); - - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - if (/^image\/.+/.test(filesToUpload[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - - attachments.push({ - ...attachment, - image_url: fileUrl[i], - image_type: filesToUpload[i].type, - image_size: filesToUpload[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl[i], - audio_type: filesToUpload[i].type, - audio_size: filesToUpload[i].size, - }); - } else if (/^video\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - video_url: fileUrl[i], - video_type: filesToUpload[i].type, - video_size: filesToUpload[i].size, - }); - } else { - attachments.push({ - ...attachment, - size: filesToUpload[i].size, - format: getFileExtension(filesToUpload[i].name), - }); - } - - const files = { - _id: _id[i], - name: filesToUpload[i].name, - type: filesToUpload[i].type, - size: filesToUpload[i].size, - }; - arrayoffiles.push(files); - } - - return e2eRoom.encryptMessageContent({ - attachments, - files: arrayoffiles, - file: filesToUpload[0], - }); - }; - - const fileContentData = { - type: filesToUpload[0].type, - typeGroup: filesToUpload[0].type.split('/')[0], - name: filesToUpload[0].name, - msg: msg || '', - encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, - }, - hashes: { - sha256: encryptedFilesarray[0].hash, - }, - }; - - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, - }; - uploadFile( - filesarray, - { - t: 'e2e', - }, - getContent, - uploadFileData, - ); - } - chat.composer?.clear(); - return; + return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; chat.composer?.clear(); @@ -671,7 +680,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} - handleFiles={handleFileUpload} + handleFiles={handleFilesToUpload} /> From d7b92cc33e29950e12d1a720b1fd597205d4ab30 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 13 Aug 2024 19:22:51 +0530 Subject: [PATCH 081/112] changed uploadFiles.ts and FileUploadModal.tsx to handle single file upload (for handing live audio sharing and video sharing) --- .../client/lib/chats/flows/uploadFiles.ts | 177 +++++++++--------- .../FileUploadModal/FileUploadModal.tsx | 121 +++--------- 2 files changed, 116 insertions(+), 182 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 2631f7e44641..32e17da8ac6f 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -31,18 +31,11 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - function updateQueue(finalFiles: File[]) { - queue.length = 0; - finalFiles.forEach((file) => { - queue.push(file); - }); - } - const uploadFile = ( - file: File[] | File, - extraData?: Pick & { msg?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + file: File, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { chat.uploads.send( file, @@ -55,9 +48,11 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ); chat.composer?.clear(); imperativeModal.close(); + uploadNextFile(); }; + const uploadNextFile = (): void => { - const file = queue[0]; + const file = queue.pop(); if (!file) { chat.composer?.dismissAllQuotedMessages(); return; @@ -67,133 +62,131 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi component: FileUploadModal, props: { file, - queue, - updateQueue, fileName: file.name, fileDescription: chat.composer?.text ?? '', showDescription: room && !isRoomFederated(room), onClose: (): void => { imperativeModal.close(); + uploadNextFile(); }, - onSubmit: async (fileName: string, msg?: string): Promise => { + onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, }); + // encrypt attachment description const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(queue, { msg }); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(queue, { msg }); + uploadFile(file, { description }); return; } - const encryptedFilesarray: any = await Promise.all(queue.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); + const encryptedFile = await e2eRoom.encryptFile(file); - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { + if (encryptedFile) { + const getContent = async (_id: string, fileUrl: string): Promise => { const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { - const attachment: FileAttachmentProps = { - title: queue[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - if (/^image\/.+/.test(queue[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(queue[i])); - - attachments.push({ - ...attachment, - image_url: fileUrl[i], - image_type: queue[i].type, - image_size: queue[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(queue[i].type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl[i], - audio_type: queue[i].type, - audio_size: queue[i].size, - }); - } else if (/^video\/.+/.test(queue[i].type)) { - attachments.push({ - ...attachment, - video_url: fileUrl[i], - video_type: queue[i].type, - video_size: queue[i].size, - }); - } else { - attachments.push({ - ...attachment, - size: queue[i].size, - format: getFileExtension(queue[i].name), - }); - } - - const files = { - _id: _id[i], - name: queue[i].name, - type: queue[i].type, - size: queue[i].size, - }; - arrayoffiles.push(files); + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + format: getFileExtension(file.name), + }); } + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ]; + return e2eRoom.encryptMessageContent({ attachments, - files: arrayoffiles, + files, file: files[0], }); }; const fileContentData = { - type: queue[0].type, - typeGroup: queue[0].type.split('/')[0], + type: file.type, + typeGroup: file.type.split('/')[0], name: fileName, - msg: msg || '', encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, + key: encryptedFile.key, + iv: encryptedFile.iv, }, hashes: { - sha256: encryptedFilesarray[0].hash, + sha256: encryptedFile.hash, }, }; - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), }; + uploadFile( - filesarray, + encryptedFile.file, { t: 'e2e', }, getContent, - uploadFileData, + fileContent, ); } }, diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index c32a59772f41..312776397b32 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,4 +1,4 @@ -import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, TextInput, Button, Scrollable, Tile, Icon } from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldError, TextInput, Button } from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -9,10 +9,8 @@ import FilePreview from './FilePreview'; type FileUploadModalProps = { onClose: () => void; - queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; - updateQueue?: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -21,8 +19,6 @@ type FileUploadModalProps = { const FileUploadModal = ({ onClose, - queue = [], - updateQueue, file, fileName, fileDescription, @@ -30,6 +26,7 @@ const FileUploadModal = ({ invalidContentType, showDescription = true, }: FileUploadModalProps): ReactElement => { + const [name, setName] = useState(fileName); const [description, setDescription] = useState(fileDescription || ''); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -37,88 +34,35 @@ const FileUploadModal = ({ const ref = useAutoFocus(); - const handleDescription = (e: ChangeEvent): void => { - setDescription(e.currentTarget.value); + const handleName = (e: ChangeEvent): void => { + setName(e.currentTarget.value); }; - const [queue1, setQueue1] = useState(queue); - const handleremove = (index: number) => { - const temp = queue1.filter((_, i) => { - return i !== index; - }); - setQueue1(temp); - }; - - const handleAddfile = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = true; - input.click(); - input.onchange = (e) => { - const target = e.target as HTMLInputElement; - const files = Array.from(target.files as FileList); - setQueue1([...queue1, ...files]); - if (updateQueue !== undefined) { - updateQueue([...queue1, ...files]); - } - }; + const handleDescription = (e: ChangeEvent): void => { + setDescription(e.currentTarget.value); }; - const handleSubmit: FormEventHandler = async (e): Promise => { + const handleSubmit: FormEventHandler = (e): void => { e.preventDefault(); - if (queue.length > 6) { - dispatchToastMessage({ + if (!name) { + return dispatchToastMessage({ type: 'error', - message: "You can't upload more than 6 files at once", + message: t('error-the-field-is-required', { field: t('Name') }), }); - onClose(); - return; } - // Iterate over each file in the queue - for (const queuedFile of queue) { - const { name: queuedFileName, size: queuedFileSize, type: queuedFileType } = queuedFile; - if (!queuedFileName) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); - return; - } - - // Validate file size - if (maxFileSize > -1 && (queuedFileSize || 0) > maxFileSize) { - onClose(); - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}+" hello testing"`, - }); - return; - } - - // Validate file content type - if (invalidContentType) { - dispatchToastMessage({ - type: 'error', - message: t('FileUpload_MediaType_NotAccepted__type__', { type: queuedFileType }), - }); - onClose(); - return; - } + // -1 maxFileSize means there is no limit + if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { + onClose(); + return dispatchToastMessage({ + type: 'error', + message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), + }); } - // description, - // msg, // Assuming msg is defined elsewhere - // }); - - // Clear the composer after each file submission - // chat.composer?.clear(); - const msg = description; - console.log('hello testing from send msg here ', msg); - onSubmit(fileName, msg); - // Close the modal after all files are submitted - // imperativeModal.close(); + onSubmit(name, description); }; + useEffect(() => { if (invalidContentType) { dispatchToastMessage({ @@ -146,13 +90,17 @@ const FileUploadModal = ({ - - - {queue1.length > 0 && - queue1.map((file, index) => )} - - + + + + + {t('Upload_file_name')} + + + + {!name && {t('error-the-field-is-required', { field: t('Name') })}} + {showDescription && ( {t('Upload_file_description')} @@ -163,19 +111,12 @@ const FileUploadModal = ({ )} - - - - + - From a0f6c832cdd6d2d12687b458dfd6662b97e63310 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 14 Aug 2024 13:35:30 +0530 Subject: [PATCH 082/112] Added the type check for the filesToUpload and also removed the references to chat in useFileUploadAction --- .../client/views/room/composer/messageBox/MessageBox.tsx | 6 ++++-- .../MessageBoxActionsToolbar.tsx | 2 +- .../hooks/useFileUploadAction.ts | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index ec0291795254..499f2e44bb9c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -100,6 +100,8 @@ type MessageBoxProps = { isEmbedded?: boolean; }; +type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; + const MessageBox = ({ tmid, onSend, @@ -126,7 +128,7 @@ const MessageBox = ({ const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFilesToUpload(filesList: File[], resetFileInput?: () => void) { + const handleFilesToUpload: HandleFilesToUpload = (filesList, resetFileInput) => { setFilesToUpload((prevFiles) => { let newFilesToUpload = [...prevFiles, ...filesList]; if (newFilesToUpload.length > 6) { @@ -163,7 +165,7 @@ const MessageBox = ({ }); resetFileInput?.(); - } + }; const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index bf6f61c77bad..698d28ad2e63 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,7 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - handleFiles?: any; + handleFiles: (filesList: File[], resetFileInput?: () => void) => void; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 304a0973ddc6..db68b7dce134 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -3,15 +3,16 @@ import { useEffect } from 'react'; import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useFileInput } from '../../../../../../hooks/useFileInput'; -import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean, handleFiles?: any): GenericMenuItemProps => { +export const useFileUploadAction = ( + disabled: boolean, + handleFiles: (filesList: File[], resetFileInput?: () => void) => void, +): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); - const chat = useChat(); useEffect(() => { const resetFileInput = () => { @@ -35,7 +36,7 @@ export const useFileUploadAction = (disabled: boolean, handleFiles?: any): Gener fileInputRef.current?.addEventListener('change', handleUploadChange); return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); - }, [chat, fileInputRef]); + }, [fileInputRef]); const handleUpload = () => { fileInputRef?.current?.click(); From de319d8829cb5c8d447f01cbdbaaa29584ba295f Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 14 Aug 2024 14:40:19 +0530 Subject: [PATCH 083/112] Added isUploading and removed the unnecessary changes --- .../file-upload/server/methods/sendFileMessage.ts | 3 ++- .../views/room/composer/messageBox/MessageBox.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 8398cdd86e90..fc2e88ab3d38 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -151,7 +151,7 @@ export const parseFileIntoMessageAttachments = async ( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendFileMessage: (roomId: string, _store: string, file: Partial[], msgData?: Record) => boolean; + sendFileMessage: (roomId: string, _store: string, file: Partial, msgData?: Record) => boolean; } } @@ -241,6 +241,7 @@ Meteor.methods({ method: 'sendFileMessage', } as any); } + return sendFileMessage(userId, { roomId, file, msgData }); }, }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 499f2e44bb9c..186be5565194 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -124,6 +124,7 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); + const [isUploading, setIsUploading] = useState(false); const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; @@ -161,6 +162,7 @@ const MessageBox = ({ return true; }); + setIsUploading(validFiles.length > 0); return validFiles; }); @@ -171,6 +173,7 @@ const MessageBox = ({ const temp = [...filesToUpload]; temp.splice(index, 1); setFilesToUpload(temp); + setIsUploading(temp.length > 0); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -384,7 +387,7 @@ const MessageBox = ({ }); const handleSendMessage = useMutableCallback(async () => { - if (filesToUpload.length > 0) { + if (isUploading) { return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; @@ -398,6 +401,7 @@ const MessageBox = ({ isSlashCommandAllowed, }); }); + const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); @@ -649,7 +653,7 @@ const MessageBox = ({ width: '100%', }} > - {filesToUpload.length > 0 && ( + {isUploading && ( <> {filesToUpload.map((file, index) => ( @@ -697,10 +701,10 @@ const MessageBox = ({ 0))} + disabled={!canSend || (!typing && !isEditing && !isUploading)} onClick={handleSendMessage} - secondary={typing || isEditing || filesToUpload.length > 0} - info={typing || isEditing || filesToUpload.length > 0} + secondary={typing || isEditing || isUploading} + info={typing || isEditing || isUploading} /> )} From 3ad5e639c238570ad8164faebd874e1da9e05925 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 15 Aug 2024 17:01:11 +0530 Subject: [PATCH 084/112] Added a folder of FilePreview near the messageBox and also changed the filePreview of modal to old view --- .../messageBox/FilePreview/FilePreview.tsx | 63 +++++++++++++++ .../messageBox/FilePreview/GenericPreview.tsx | 68 ++++++++++++++++ .../messageBox/FilePreview/ImagePreview.tsx | 62 +++++++++++++++ .../messageBox/FilePreview/MediaPreview.tsx | 78 +++++++++++++++++++ .../FilePreview/PreviewSkeleton.tsx | 7 ++ .../room/composer/messageBox/MessageBox.tsx | 2 +- .../modals/FileUploadModal/FilePreview.tsx | 15 +--- .../modals/FileUploadModal/GenericPreview.tsx | 68 ++-------------- .../modals/FileUploadModal/ImagePreview.tsx | 33 ++------ .../modals/FileUploadModal/MediaPreview.tsx | 24 +++--- 10 files changed, 308 insertions(+), 112 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx new file mode 100644 index 000000000000..959f30aaccc4 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx @@ -0,0 +1,63 @@ +import type { ReactElement } from 'react'; +import React from 'react'; + +import { isIE11 } from '../../../../../lib/utils/isIE11'; +import GenericPreview from './GenericPreview'; +import MediaPreview from './MediaPreview'; + +export enum FilePreviewType { + IMAGE = 'image', + AUDIO = 'audio', + // VIDEO = 'video', // currently showing it in simple generic view +} + +const getFileType = (fileType: File['type']): FilePreviewType | undefined => { + if (!fileType) { + return; + } + for (const type of Object.values(FilePreviewType)) { + if (fileType.indexOf(type) > -1) { + return type; + } + } +}; + +const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefined): boolean => { + if (!fileType) { + return false; + } + if (isIE11) { + return false; + } + // Avoid preview if file size bigger than 10mb + if (file.size > 10000000) { + return false; + } + if (!Object.values(FilePreviewType).includes(fileType)) { + return false; + } + return true; +}; + +type FilePreviewProps = { + file: File; + key: number; + index: number; + onRemove: (index: number) => void; +}; + +const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { + const fileType = getFileType(file.type); + + const handleRemove = () => { + onRemove(index); + }; + + if (shouldShowMediaPreview(file, fileType)) { + return ; + } + + return ; +}; + +export default FilePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx new file mode 100644 index 000000000000..977d17d7205f --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -0,0 +1,68 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; + +import { formatBytes } from '../../../../../lib/utils/formatBytes'; + +type GenericPreviewProps = { + file: File; + index: number; + onRemove: (index: number) => void; +}; + +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const buttonStyle: React.CSSProperties = { + position: 'absolute' as const, + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + return ( + + + {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( + + ) : ( + + )} + + + {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} + {`${file.name.split('.')[1]}`} + + {/*
*/} + onRemove(index)} /> + + ); +}; +export default GenericPreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx new file mode 100644 index 000000000000..de15c8444acb --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -0,0 +1,62 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; + +import GenericPreview from './GenericPreview'; +import PreviewSkeleton from './PreviewSkeleton'; + +type ImagePreviewProps = { + url: string; + file: File; + onRemove: (index: number) => void; + index: number; +}; + +const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { + const [error, setError] = useState(false); + const [loading, setLoading] = useState(true); + const [isHovered, setIsHovered] = useState(false); + + const handleLoad = (): void => setLoading(false); + const handleError = (): void => { + setLoading(false); + setError(true); + }; + + const handleMouseEnter = (): void => setIsHovered(true); + const handleMouseLeave = (): void => setIsHovered(false); + + const buttonStyle: React.CSSProperties = { + position: 'absolute', + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + + if (error) { + return ; + } + + return ( + + {loading && } + + onRemove(index)} /> + + ); +}; + +export default ImagePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx new file mode 100644 index 000000000000..a5eed5a03db3 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx @@ -0,0 +1,78 @@ +import { AudioPlayer, Box, Icon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { useEffect, useState, memo } from 'react'; + +// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view +import { FilePreviewType } from './FilePreview'; +import ImagePreview from './ImagePreview'; +import PreviewSkeleton from './PreviewSkeleton'; + +type ReaderOnloadCallback = (url: FileReader['result']) => void; + +const readFileAsDataURL = (file: File, callback: ReaderOnloadCallback): void => { + const reader = new FileReader(); + reader.onload = (e): void => callback(e?.target?.result || null); + + return reader.readAsDataURL(file); +}; + +const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader['result']] => { + const [loaded, setLoaded] = useState(false); + const [url, setUrl] = useState(null); + + useEffect(() => { + setLoaded(false); + readFileAsDataURL(file, (url) => { + setUrl(url); + setLoaded(true); + }); + }, [file]); + return [loaded, url]; +}; + +type MediaPreviewProps = { + file: File; + fileType: FilePreviewType; + onRemove: (index: number) => void; + index: number; +}; + +const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { + const [loaded, url] = useFileAsDataURL(file); + const t = useTranslation(); + + if (!loaded) { + return ; + } + + if (typeof url !== 'string') { + return ( + + + {t('FileUpload_Cannot_preview_file')} + + ); + } + + if (fileType === FilePreviewType.IMAGE) { + return ; + } + + // if (fileType === FilePreviewType.VIDEO) { + // return ( + // + // + // {t('Browser_does_not_support_video_element')} + // + // ); + // } + + if (fileType === FilePreviewType.AUDIO) { + return ; + } + + throw new Error('Wrong props provided for MediaPreview'); +}; + +export default memo(MediaPreview); diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx new file mode 100644 index 000000000000..72017e26e9f5 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx @@ -0,0 +1,7 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React from 'react'; + +const PreviewSkeleton = (): ReactElement => ; + +export default PreviewSkeleton; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 186be5565194..89add073785d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -49,7 +49,7 @@ import fileSize from 'filesize'; import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { Box } from '@rocket.chat/fuselage'; -import FilePreview from '../../modals/FileUploadModal/FilePreview'; +import FilePreview from './FilePreview/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index 57a0cf8e47b5..c898c8b0081d 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -8,7 +8,7 @@ import MediaPreview from './MediaPreview'; export enum FilePreviewType { IMAGE = 'image', AUDIO = 'audio', - // VIDEO = 'video', // currently showing it in simple generic view + VIDEO = 'video', } const getFileType = (fileType: File['type']): FilePreviewType | undefined => { @@ -41,23 +41,16 @@ const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefine type FilePreviewProps = { file: File; - key: number; - index: number; - onRemove: (index: number) => void; }; -const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { +const FilePreview = ({ file }: FilePreviewProps): ReactElement => { const fileType = getFileType(file.type); - const handleRemove = () => { - onRemove(index); - }; - if (shouldShowMediaPreview(file, fileType)) { - return ; + return ; } - return ; + return ; }; export default FilePreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index b7d2367fc643..dd251bec6769 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,68 +1,14 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import { formatBytes } from '../../../../lib/utils/formatBytes'; -type GenericPreviewProps = { - file: File; - index: number; - onRemove: (index: number) => void; -}; +const GenericPreview = ({ file }: { file: File }): ReactElement => ( + + + {`${file.name} - ${formatBytes(file.size, 2)}`} + +); -const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { - const [isHovered, setIsHovered] = useState(false); - - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - const buttonStyle: React.CSSProperties = { - position: 'absolute' as const, - right: 0, - top: 0, - backgroundColor: 'gray', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - return ( - - - {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( - - ) : ( - - )} - - - {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} - {`${file.name.split('.')[1]}`} - - {/*
*/} - onRemove(index)} /> - - ); -}; export default GenericPreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx index de15c8444acb..a0b012167671 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; +import { Box } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -8,14 +8,11 @@ import PreviewSkeleton from './PreviewSkeleton'; type ImagePreviewProps = { url: string; file: File; - onRemove: (index: number) => void; - index: number; }; -const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { +const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { const [error, setError] = useState(false); const [loading, setLoading] = useState(true); - const [isHovered, setIsHovered] = useState(false); const handleLoad = (): void => setLoading(false); const handleError = (): void => { @@ -23,39 +20,23 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE setError(true); }; - const handleMouseEnter = (): void => setIsHovered(true); - const handleMouseLeave = (): void => setIsHovered(false); - - const buttonStyle: React.CSSProperties = { - position: 'absolute', - right: 0, - top: 0, - backgroundColor: 'gray', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - if (error) { - return ; + return ; } return ( - + <> {loading && } - onRemove(index)} /> - + ); }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index a5eed5a03db3..6b48a47a79ef 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useState, memo } from 'react'; -// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view +import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; import { FilePreviewType } from './FilePreview'; import ImagePreview from './ImagePreview'; import PreviewSkeleton from './PreviewSkeleton'; @@ -34,11 +34,9 @@ const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader[ type MediaPreviewProps = { file: File; fileType: FilePreviewType; - onRemove: (index: number) => void; - index: number; }; -const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { +const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { const [loaded, url] = useFileAsDataURL(file); const t = useTranslation(); @@ -56,17 +54,17 @@ const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): R } if (fileType === FilePreviewType.IMAGE) { - return ; + return ; } - // if (fileType === FilePreviewType.VIDEO) { - // return ( - // - // - // {t('Browser_does_not_support_video_element')} - // - // ); - // } + if (fileType === FilePreviewType.VIDEO) { + return ( + + + {t('Browser_does_not_support_video_element')} + + ); + } if (fileType === FilePreviewType.AUDIO) { return ; From 72bc00d938918a4a9b77afc29b100fda7e78c4d4 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 16 Aug 2024 23:17:45 +0530 Subject: [PATCH 085/112] feat: Enable file attachments after typing a message --- .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 698d28ad2e63..2ce667bd866a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -59,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handleFiles); + const fileUploadAction = useFileUploadAction(!canSend || isRecording, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); From b3db9fcccaf882083320ee5fcb9088157684e59a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 17 Aug 2024 01:21:06 +0530 Subject: [PATCH 086/112] added title to the generic and image preview --- .../room/composer/messageBox/FilePreview/GenericPreview.tsx | 1 + .../views/room/composer/messageBox/FilePreview/ImagePreview.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx index 977d17d7205f..67bc0a5b6c45 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -40,6 +40,7 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl position='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + title={file.name} > {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx index de15c8444acb..d73366a45f05 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -43,7 +43,7 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE } return ( - + {loading && } Date: Sat, 17 Aug 2024 12:41:23 +0530 Subject: [PATCH 087/112] fix: issue while sharing the message after file shared --- apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 89add073785d..25e070e32283 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -388,6 +388,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (isUploading) { + setIsUploading(!isUploading); return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; From 11aac8e2d05819dda44f77fd453e56f1a70c942f Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 18 Aug 2024 13:50:25 +0530 Subject: [PATCH 088/112] passed uploadIdsToConfirm to the sendMessage in executeSendMessage --- .../app/lib/server/functions/sendMessage.ts | 47 +++++++++++++---- .../app/lib/server/methods/sendMessage.ts | 50 +++++-------------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index cd9febd58706..8777178e8f54 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,11 +1,13 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; +import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; +import { API } from '../../../api/server/api'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; @@ -222,22 +224,45 @@ export const sendMessage = async function ( room: any, upsert = false, previewUrls?: string[], - filesArray?: Partial[] | Partial, + uploadIdsToConfirm?: string[], ) { if (!user || !message || !room._id) { return false; } - if (filesArray !== undefined && (typeof filesArray !== undefined || message?.t !== 'e2e')) { - const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments( - Array.isArray(filesArray) ? filesArray : [filesArray], - roomId, - user, + if (uploadIdsToConfirm !== undefined) { + if (!(await canAccessRoomIdAsync(message.rid, user._id))) { + return API.v1.unauthorized(); + } + const filesToConfirm: Partial[] = await Promise.all( + uploadIdsToConfirm.map(async (fileid) => { + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + return file; + }), ); - message.file = files[0]; - message.files = files; - message.attachments = attachments; + + // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); + if (message?.t !== 'e2e') { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments(filesToConfirm, roomId, user); + // message.file = files[0]; // as uploading multiple files + message.files = files; + message.attachments = attachments; + } + + await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); + + // let resmessage; + // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { + // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); + // } + + // return API.v1.success({ + // resmessage, + // }); } await validateMessage(message, room, user); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index e172216edb2d..9fa1dfbdfc40 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,15 +1,13 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser, IUpload } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Uploads, Users } from '@rocket.chat/models'; +import { Messages, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { API } from '../../../api/server/api'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { metrics } from '../../../metrics/server'; @@ -22,7 +20,7 @@ export async function executeSendMessage( uid: IUser['_id'], message: AtLeast, previewUrls?: string[], - filesArray?: Partial[], + uploadIdsToConfirm?: string[], ) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { @@ -100,7 +98,7 @@ export async function executeSendMessage( } metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - return await sendMessage(user, message, room, false, previewUrls, filesArray); + return await sendMessage(user, message, room, false, previewUrls, uploadIdsToConfirm); } catch (err: any) { SystemLogger.error({ msg: 'Error sending message:', err }); @@ -121,12 +119,17 @@ export async function executeSendMessage( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendMessage(message: AtLeast, previewUrls?: string[], filesToConfirm?: string[], msgData?: any): any; + sendMessage( + message: AtLeast, + previewUrls?: string[], + uploadIdsToConfirm?: string[], + msgData?: any, + ): any; } } Meteor.methods({ - async sendMessage(message, previewUrls, filesToConfirm, msgData) { + async sendMessage(message, previewUrls, uploadIdsToConfirm, msgData) { check(message, Object); const uid = Meteor.userId(); @@ -139,42 +142,15 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } - let filesArray: Partial[] = []; - - if (filesToConfirm !== undefined) { - if (!(await canAccessRoomIdAsync(message.rid, uid))) { - return API.v1.unauthorized(); - } - filesArray = await Promise.all( - filesToConfirm.map(async (fileid) => { - const file = await Uploads.findOneById(fileid); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - return file; - }), - ); + if (msgData !== undefined) { message.msg = msgData?.msg; message.tmid = msgData?.tmid; - // description, message.t = msgData?.t; message.content = msgData?.content; - // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); - - await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); - - // let resmessage; - // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { - // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); - // } - - // return API.v1.success({ - // resmessage, - // }); } try { - return await executeSendMessage(uid, message, previewUrls, filesArray); + return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { if ((error.error || error.message) === 'error-not-allowed') { throw new Meteor.Error(error.error || error.message, error.reason, { From 9ff1b9f8020c74d32b68fe8e2a70100600d5b042 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 18 Aug 2024 17:58:53 +0530 Subject: [PATCH 089/112] Added newe function parseMultipleFilesIntoMessageAttachments --- .../server/methods/sendFileMessage.ts | 125 ++++++++++++++++-- .../app/lib/server/functions/sendMessage.ts | 4 +- 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index fc2e88ab3d38..50d82017f613 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -16,7 +16,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; -// import { SystemLogger } from '../../../../server/lib/logger/system'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; @@ -30,17 +30,10 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } -export const parseFileIntoMessageAttachments = async ( - filearr: Partial[] | Partial, - roomId: string, - user: IUser, -): Promise => { +export const parseMultipleFilesIntoMessageAttachments = async (files: Partial[], user: IUser): Promise => { const attachments: MessageAttachment[] = []; const filesarray: FileProp[] = []; - if (!Array.isArray(filearr)) { - filearr = [filearr]; - } - filearr.forEach(async (file: Partial) => { + files.forEach(async (file: Partial) => { validateFileRequiredFields(file); await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); @@ -148,6 +141,118 @@ export const parseFileIntoMessageAttachments = async ( return { files: filesarray, attachments }; }; +export const parseFileIntoMessageAttachments = async ( + file: Partial, + roomId: string, + user: IUser, +): Promise => { + validateFileRequiredFields(file); + + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + + const attachments: MessageAttachment[] = []; + + const files = [ + { + _id: file._id, + name: file.name || '', + type: file.type || 'file', + size: file.size || 0, + format: file.identify?.format || '', + }, + ]; + + if (/^image\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type as string, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; + } + + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + } + } catch (e) { + SystemLogger.error(e); + } + attachments.push(attachment); + } else if (/^audio\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type as string, + audio_size: file.size, + }; + attachments.push(attachment); + } else if (/^video\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type as string, + video_size: file.size as number, + }; + attachments.push(attachment); + } else { + const attachment = { + title: file.name, + type: 'file', + format: getFileExtension(file.name), + description: file.description, + title_link: fileUrl, + title_link_download: true, + size: file.size as number, + }; + attachments.push(attachment); + } + return { files, attachments }; +}; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 8777178e8f54..e3b4b5d37ec5 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -15,7 +15,7 @@ import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { parseMultipleFilesIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -247,7 +247,7 @@ export const sendMessage = async function ( // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); if (message?.t !== 'e2e') { const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments(filesToConfirm, roomId, user); + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, user); // message.file = files[0]; // as uploading multiple files message.files = files; message.attachments = attachments; From 2370b14ca8185ebbd1610a5f8a7c10309008f9de Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 19 Aug 2024 21:50:57 +0530 Subject: [PATCH 090/112] added ui changes and also added the transition --- .../messageBox/FilePreview/GenericPreview.tsx | 30 ++++++++----- .../messageBox/FilePreview/ImagePreview.tsx | 12 ++--- .../room/composer/messageBox/MessageBox.tsx | 45 ++++++++++++------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx index 67bc0a5b6c45..e8a4112a8bf2 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -23,9 +23,9 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl const buttonStyle: React.CSSProperties = { position: 'absolute' as const, - right: 0, - top: 0, - backgroundColor: 'gray', + right: '-10px', + top: '-8px', + backgroundColor: '#6d6c6c', display: isHovered ? 'block' : 'none', cursor: 'pointer', zIndex: 1, @@ -35,34 +35,42 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl return ( - + {/* currently using div */} +
{file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( - + ) : ( - + )} - +
{`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} - {`${file.name.split('.')[1]}`} + {`${file.name.split('.')[1]}`} {/*
*/} - onRemove(index)} /> + onRemove(index)} /> ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx index d73366a45f05..9c4e54c7df6f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -27,10 +27,10 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE const handleMouseLeave = (): void => setIsHovered(false); const buttonStyle: React.CSSProperties = { - position: 'absolute', - right: 0, - top: 0, - backgroundColor: 'gray', + position: 'absolute' as const, + right: '-10px', + top: '-8px', + backgroundColor: '#6d6c6c', display: isHovered ? 'block' : 'none', cursor: 'pointer', zIndex: 1, @@ -49,12 +49,12 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE is='img' src={url} maxWidth='150px' - style={{ objectFit: 'contain', borderRadius: '10px' }} + style={{ objectFit: 'contain', borderRadius: '10px', height: '45px' }} onLoad={handleLoad} onError={handleError} display={loading ? 'none' : 'initial'} /> - onRemove(index)} /> + onRemove(index)} /> ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 25e070e32283..3bf14496ab73 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -168,12 +168,22 @@ const MessageBox = ({ resetFileInput?.(); }; + const handleRemoveFile = (indexToRemove: number) => { + const updatedFiles = [...filesToUpload]; - const handleRemoveFile = (index: number) => { - const temp = [...filesToUpload]; - temp.splice(index, 1); - setFilesToUpload(temp); - setIsUploading(temp.length > 0); + const element = document.getElementById(`file-preview-${indexToRemove}`); + if (element) { + element.style.transition = 'opacity 0.3s ease-in-out'; + element.style.opacity = '0'; + } + + setTimeout(() => { + updatedFiles.splice(indexToRemove, 1); + setFilesToUpload(updatedFiles); + if (element) { + element.style.opacity = '1'; + } + }, 300); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -649,18 +659,21 @@ const MessageBox = ({
- {isUploading && ( - <> - {filesToUpload.map((file, index) => ( - - ))} - - )} + {filesToUpload.map((file, index) => ( +
+ +
+ ))}
From 972219418807dce7325b38c202375ba3ebd3521b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 21 Aug 2024 18:19:54 +0530 Subject: [PATCH 091/112] shifted confirm files at last after save message and attached multiple files by calling parseFileIntoMessageAttachments multiple times --- .../server/methods/sendFileMessage.ts | 182 ++++-------------- .../app/lib/server/functions/sendMessage.ts | 41 ++-- 2 files changed, 53 insertions(+), 170 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 50d82017f613..49299c259b60 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,7 +6,6 @@ import type { AtLeast, FilesAndAttachments, IMessage, - FileProp, } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; @@ -30,117 +29,6 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } -export const parseMultipleFilesIntoMessageAttachments = async (files: Partial[], user: IUser): Promise => { - const attachments: MessageAttachment[] = []; - const filesarray: FileProp[] = []; - files.forEach(async (file: Partial) => { - validateFileRequiredFields(file); - - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - - const files = [ - { - _id: file._id, - name: file.name || '', - type: file.type || 'file', - size: file.size || 0, - format: file.identify?.format || '', - }, - ]; - - if (/^image\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file?.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type as string, - image_size: file.size, - }; - - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; - } - - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // } - // } catch (e) { - // SystemLogger.error(e); - // } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type as string, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type as string, - video_size: file.size as number, - }; - attachments.push(attachment); - } else { - const attachment = { - title: file.name, - type: 'file', - format: getFileExtension(file.name), - description: file.description, - title_link: fileUrl, - title_link_download: true, - size: file.size as number, - }; - attachments.push(attachment); - } - filesarray.push(...files); - }); - return { files: filesarray, attachments }; -}; - export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, @@ -180,39 +68,43 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); - } - } catch (e) { - SystemLogger.error(e); - } + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // console.log('attachement image preview ' + attachment.image_preview); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // console.log('thumbResult ' + thumbResult); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // console.log('thumbnail ' + thumbnail); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // console.log(files); + // } + // } catch (e) { + // SystemLogger.error(e); + // } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index e3b4b5d37ec5..09f502e4fbc9 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,13 +1,11 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; -import { API } from '../../../api/server/api'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; @@ -15,7 +13,7 @@ import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseMultipleFilesIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -231,9 +229,6 @@ export const sendMessage = async function ( } if (uploadIdsToConfirm !== undefined) { - if (!(await canAccessRoomIdAsync(message.rid, user._id))) { - return API.v1.unauthorized(); - } const filesToConfirm: Partial[] = await Promise.all( uploadIdsToConfirm.map(async (fileid) => { const file = await Uploads.findOneById(fileid); @@ -243,26 +238,18 @@ export const sendMessage = async function ( return file; }), ); - - // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); if (message?.t !== 'e2e') { - const roomId = message.rid; - const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, user); - // message.file = files[0]; // as uploading multiple files - message.files = files; - message.attachments = attachments; + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + filesToConfirm.forEach(async (file) => { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + message.files = messageFiles; + message.attachments = messageAttachments; } - - await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); - - // let resmessage; - // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { - // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); - // } - - // return API.v1.success({ - // resmessage, - // }); } await validateMessage(message, room, user); @@ -337,6 +324,10 @@ export const sendMessage = async function ( // TODO: is there an opportunity to send returned data to notifyOnMessageChange? await afterSaveMessage(message, room); + if (uploadIdsToConfirm !== undefined) { + await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); + } + void notifyOnMessageChange({ id: message._id }); void notifyOnRoomChangedById(message.rid); From 84bfcae6f3f13e2dce988554b80df2289d1fa57b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 22 Aug 2024 17:03:16 +0530 Subject: [PATCH 092/112] fix: image thumbnail display and remove extra msgData parameter --- .../server/methods/sendFileMessage.ts | 74 +++++++++---------- .../app/lib/server/functions/sendMessage.ts | 6 +- .../app/lib/server/methods/sendMessage.ts | 16 +--- apps/meteor/client/lib/chats/uploads.ts | 21 ++---- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 49299c259b60..e9fadc442e78 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -68,43 +68,43 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // console.log('attachement image preview ' + attachment.image_preview); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // console.log('thumbResult ' + thumbResult); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // console.log('thumbnail ' + thumbnail); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // console.log(files); - // } - // } catch (e) { - // SystemLogger.error(e); - // } + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + console.log('attachement image preview ' + attachment.image_preview); + const thumbResult = await FileUpload.createImageThumbnail(file); + console.log('thumbResult ' + thumbResult); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + console.log('thumbnail ' + thumbnail); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + console.log(files); + } + } catch (e) { + SystemLogger.error(e); + } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 09f502e4fbc9..c2d4bdc7e248 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -241,9 +241,13 @@ export const sendMessage = async function ( if (message?.t !== 'e2e') { const messageFiles: FileProp[] = []; const messageAttachments: MessageAttachment[] = []; - filesToConfirm.forEach(async (file) => { + const fileAttachmentPromises = filesToConfirm.map(async (file) => { const roomId = message.rid; const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + }); + const filesAndAttachments = await Promise.all(fileAttachmentPromises); + filesAndAttachments.forEach(({ files, attachments }) => { messageFiles.push(...files); messageAttachments.push(...attachments); }); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 9fa1dfbdfc40..b91e9951f00b 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -119,17 +119,12 @@ export async function executeSendMessage( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendMessage( - message: AtLeast, - previewUrls?: string[], - uploadIdsToConfirm?: string[], - msgData?: any, - ): any; + sendMessage(message: AtLeast, previewUrls?: string[], uploadIdsToConfirm?: string[]): any; } } Meteor.methods({ - async sendMessage(message, previewUrls, uploadIdsToConfirm, msgData) { + async sendMessage(message, previewUrls, uploadIdsToConfirm) { check(message, Object); const uid = Meteor.userId(); @@ -142,13 +137,6 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } - if (msgData !== undefined) { - message.msg = msgData?.msg; - message.tmid = msgData?.tmid; - message.t = msgData?.t; - message.content = msgData?.content; - } - try { return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index d1116d580af4..7e94cbe2a45f 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -123,28 +123,23 @@ const send = async ( msg = ''; } - const text: IMessage = { - rid, - _id: id, - msg: '', - ts: new Date(), - u: { _id: id, username: id }, - _updatedAt: new Date(), - }; - try { let content; if (getContent) { content = await getContent(fileIds, fileUrls); } - const msgData = { - msg, + const text: IMessage = { + rid, + _id: id, + msg: msg || '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), tmid, - description, t, content, }; - await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); + await sdk.call('sendMessage', text, fileUrls, fileIds); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { From 335e886cf8ebb9b5ac22531bb08686d932b06cfc Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 22 Aug 2024 19:59:35 +0530 Subject: [PATCH 093/112] removed console logs --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index e9fadc442e78..73dfd0216a73 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -70,9 +70,7 @@ export const parseFileIntoMessageAttachments = async ( try { attachment.image_preview = await FileUpload.resizeImagePreview(file); - console.log('attachement image preview ' + attachment.image_preview); const thumbResult = await FileUpload.createImageThumbnail(file); - console.log('thumbResult ' + thumbResult); if (thumbResult) { const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; const thumbnail = await FileUpload.uploadImageThumbnail( @@ -85,7 +83,6 @@ export const parseFileIntoMessageAttachments = async ( roomId, user._id, ); - console.log('thumbnail ' + thumbnail); const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); attachment.image_url = thumbUrl; attachment.image_type = thumbnail.type; @@ -100,7 +97,6 @@ export const parseFileIntoMessageAttachments = async ( size: thumbnail.size || 0, format: thumbnail.identify?.format || '', }); - console.log(files); } } catch (e) { SystemLogger.error(e); From a5598f7b191a10387b6831532848346782e03d68 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 00:22:27 +0530 Subject: [PATCH 094/112] added a function for parse and also solved eslint error of reshuffling --- .../app/lib/server/functions/sendMessage.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index c2d4bdc7e248..54a66a4988ff 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment, IUser } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -8,12 +8,12 @@ import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; +import { parseFileIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -213,6 +213,39 @@ export function prepareMessageObject( } } +const parseFilesIntoMessageAttachments = async ( + filesToConfirm: Partial[], + roomId: string, + user: IUser, +): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + + const fileAttachmentPromises = filesToConfirm.map(async (file) => { + try { + if (!file) { + console.warn('Skipping undefined file'); + return { files: [], attachments: [] }; + } + + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + } catch (error) { + console.error('Error processing file:', file, error); + return { files: [], attachments: [] }; + } + }); + + const filesAndAttachments = await Promise.all(fileAttachmentPromises); + + filesAndAttachments.forEach(({ files, attachments }) => { + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + + return { files: messageFiles, attachments: messageAttachments }; +}; + /** * Validates and sends the message object. */ @@ -239,20 +272,9 @@ export const sendMessage = async function ( }), ); if (message?.t !== 'e2e') { - const messageFiles: FileProp[] = []; - const messageAttachments: MessageAttachment[] = []; - const fileAttachmentPromises = filesToConfirm.map(async (file) => { - const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - return { files, attachments }; - }); - const filesAndAttachments = await Promise.all(fileAttachmentPromises); - filesAndAttachments.forEach(({ files, attachments }) => { - messageFiles.push(...files); - messageAttachments.push(...attachments); - }); - message.files = messageFiles; - message.attachments = messageAttachments; + const { files, attachments } = await parseFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + message.files = files; + message.attachments = attachments; } } From 7ea18d70e49f7526bcdcb5dfb2cb5b9e1dd0696a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 12:01:36 +0530 Subject: [PATCH 095/112] fix: solved lint errors messageBox and uploadfiles --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 + apps/meteor/client/lib/chats/flows/uploadFiles.ts | 8 ++++---- apps/meteor/client/lib/chats/uploads.ts | 14 ++++++++++---- .../views/room/composer/messageBox/MessageBox.tsx | 10 +++++----- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index b91e9951f00b..dcb203275bf4 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -137,6 +137,7 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } + try { return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 32e17da8ac6f..dde1eaee1f50 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -33,7 +33,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { @@ -79,14 +79,14 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { description }); + uploadFile(file); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { description }); + uploadFile(file); return; } @@ -99,7 +99,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: file.name, type: 'file', - description, title_link: fileUrl, title_link_download: true, encryption: { @@ -166,6 +165,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, + msg: description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 7e94cbe2a45f..4920d93a19f8 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -32,13 +32,11 @@ const wipeFailedOnes = (): void => { const send = async ( file: File[] | File, { - description, msg, rid, tmid, t, }: { - description?: string; msg?: string; rid: string; tmid?: string; @@ -141,6 +139,14 @@ const send = async ( }; await sdk.call('sendMessage', text, fileUrls, fileIds); + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + // msg, + // tmid, + // description, + // t, + // content, + // }); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { updateUploads((uploads) => @@ -181,8 +187,8 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File[] | File, - { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, + { msg, t }: { msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), + ): Promise => send(file, { msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 3bf14496ab73..5d4913cbab9b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,6 @@ /* eslint-disable complexity */ import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction, @@ -14,15 +15,18 @@ import { } from '@rocket.chat/ui-composer'; import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import fileSize from 'filesize'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; +import { e2e } from '../../../../../app/e2e/client'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; @@ -40,16 +44,12 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; +import FilePreview from './FilePreview/FilePreview'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; -import fileSize from 'filesize'; -import { e2e } from '../../../../../app/e2e/client'; -import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; -import { Box } from '@rocket.chat/fuselage'; -import FilePreview from './FilePreview/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; From 822e2c5d8b70139d703405f8f1b2d859104cedc6 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 14:36:18 +0530 Subject: [PATCH 096/112] fix: lint and TS errors --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 6 ++- .../room/composer/messageBox/MessageBox.tsx | 39 +++++++++++-------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 0fa55d4dbcc1..45b9dc7c8bf3 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { msg, t, e2e }: { msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index dde1eaee1f50..6424ae25d7f0 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,7 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, extraData?: Pick & { msg?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { chat.uploads.send( @@ -93,8 +93,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string): Promise => { + const getContent = async (filesId: string[], filesUrl: string[]): Promise => { const attachments = []; + const _id = filesId[0]; + const fileUrl = filesUrl[0]; const attachment: FileAttachmentProps = { title: file.name, diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 5d4913cbab9b..9de73683943e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -226,9 +226,10 @@ const MessageBox = ({ if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { + const attachments: FileAttachmentProps[] = []; + const arrayoffiles: any = []; + + const promises = _id.map(async (id, i) => { const attachment: FileAttachmentProps = { title: filesToUpload[i].name, type: 'file', @@ -243,10 +244,11 @@ const MessageBox = ({ }, }; + let updatedAttachment; + if (/^image\/.+/.test(filesToUpload[i].type)) { const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - - attachments.push({ + updatedAttachment = { ...attachment, image_url: fileUrl[i], image_type: filesToUpload[i].type, @@ -254,38 +256,43 @@ const MessageBox = ({ ...(dimensions && { image_dimensions: dimensions, }), - }); + }; } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - attachments.push({ + updatedAttachment = { ...attachment, audio_url: fileUrl[i], audio_type: filesToUpload[i].type, audio_size: filesToUpload[i].size, - }); + }; } else if (/^video\/.+/.test(filesToUpload[i].type)) { - attachments.push({ + updatedAttachment = { ...attachment, video_url: fileUrl[i], video_type: filesToUpload[i].type, video_size: filesToUpload[i].size, - }); + }; } else { - attachments.push({ + updatedAttachment = { ...attachment, size: filesToUpload[i].size, format: getFileExtension(filesToUpload[i].name), - }); + }; } + attachments.push(updatedAttachment); + const files = { - _id: _id[i], + _id: id, name: filesToUpload[i].name, type: filesToUpload[i].type, size: filesToUpload[i].size, }; - arrayoffiles.push(files); - } + arrayoffiles.push(files); + }); + await Promise.all(promises); + console.log('messageBox attachments ' + attachments); + console.log('messgaeBox files ' + arrayoffiles); return e2eRoom.encryptMessageContent({ attachments, files: arrayoffiles, @@ -353,7 +360,6 @@ const MessageBox = ({ handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); chat.composer?.clear(); setFilesToUpload([]); - return; }; const { isMobile } = useLayout(); @@ -665,6 +671,7 @@ const MessageBox = ({ > {filesToUpload.map((file, index) => (
Date: Fri, 23 Aug 2024 15:23:24 +0530 Subject: [PATCH 097/112] fix: lint error and converted description to msg --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 10 ++++++++-- apps/meteor/client/lib/chats/uploads.ts | 17 +++++------------ .../room/composer/messageBox/MessageBox.tsx | 9 ++++----- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 45b9dc7c8bf3..b963bc8550be 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { msg, t, e2e }: { msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 6424ae25d7f0..c4440afe4e3e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -2,6 +2,7 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rock import { isRoomFederated } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; +import { settings } from '../../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; @@ -79,14 +80,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file); + uploadFile(file, { msg: description }); + return; + } + + if (!settings.get('E2E_Enable_Encrypt_Files')) { + uploadFile(file, { msg: description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file); + uploadFile(file, { msg: description }); return; } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 4920d93a19f8..ad29d514bc32 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -32,11 +32,13 @@ const wipeFailedOnes = (): void => { const send = async ( file: File[] | File, { + description, msg, rid, tmid, t, }: { + description?: string; msg?: string; rid: string; tmid?: string; @@ -129,7 +131,7 @@ const send = async ( const text: IMessage = { rid, _id: id, - msg: msg || '', + msg: msg || description || '', ts: new Date(), u: { _id: id, username: id }, _updatedAt: new Date(), @@ -138,15 +140,6 @@ const send = async ( content, }; await sdk.call('sendMessage', text, fileUrls, fileIds); - - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - // msg, - // tmid, - // description, - // t, - // content, - // }); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { updateUploads((uploads) => @@ -187,8 +180,8 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File[] | File, - { msg, t }: { msg?: string; t?: IMessage['t'] }, + { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => send(file, { msg, rid, tmid, t }, getContent, fileContent), + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 9de73683943e..63ad32d37214 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -227,7 +227,7 @@ const MessageBox = ({ if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments: FileAttachmentProps[] = []; - const arrayoffiles: any = []; + const uploadFiles: any = []; const promises = _id.map(async (id, i) => { const attachment: FileAttachmentProps = { @@ -288,14 +288,13 @@ const MessageBox = ({ size: filesToUpload[i].size, }; - arrayoffiles.push(files); + uploadFiles.push(files); }); await Promise.all(promises); - console.log('messageBox attachments ' + attachments); - console.log('messgaeBox files ' + arrayoffiles); + return e2eRoom.encryptMessageContent({ attachments, - files: arrayoffiles, + files: uploadFiles, file: filesToUpload[0], }); }; From b13bdd230f84632baf925663403c62def1847fd9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 15:52:20 +0530 Subject: [PATCH 098/112] fix: added description for TS error --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index c4440afe4e3e..630b2a39b27b 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,7 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { msg?: string }, + extraData?: Pick & { description?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { @@ -80,19 +80,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } if (!settings.get('E2E_Enable_Encrypt_Files')) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } @@ -107,6 +107,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: file.name, type: 'file', + description, title_link: fileUrl, title_link_download: true, encryption: { @@ -173,7 +174,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, - msg: description, + description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, From 0fe3aee5ee8981e9b493c1449f93fcb8105544fd Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 24 Aug 2024 15:23:44 +0530 Subject: [PATCH 099/112] Changed the location of parsing file into attachments and also added filter to it --- .../server/methods/sendFileMessage.ts | 33 ++++++++++++++++ .../app/lib/server/functions/sendMessage.ts | 39 ++----------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 73dfd0216a73..c63bb0e9e2ef 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,6 +6,7 @@ import type { AtLeast, FilesAndAttachments, IMessage, + FileProp, } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; @@ -29,6 +30,38 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } +export const parseMultipleFilesIntoMessageAttachments = async ( + filesToConfirm: Partial[], + roomId: string, + user: IUser, +): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + + const filesAndAttachments = await Promise.all( + filesToConfirm + .filter((files: Partial) => !!files) + .map(async (file) => { + try { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + } catch (error) { + console.error('Error processing file:', file, error); + return { files: [], attachments: [] }; + } + }), + ); + + filesAndAttachments + .filter(({ files, attachments }) => files.length || attachments.length) + .forEach(({ files, attachments }) => { + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + + return { files: messageFiles, attachments: messageAttachments }; +}; + export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 54a66a4988ff..cfcf21eb29c0 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -8,7 +8,7 @@ import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; -import { parseFileIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; +import { parseMultipleFilesIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; @@ -213,39 +213,6 @@ export function prepareMessageObject( } } -const parseFilesIntoMessageAttachments = async ( - filesToConfirm: Partial[], - roomId: string, - user: IUser, -): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { - const messageFiles: FileProp[] = []; - const messageAttachments: MessageAttachment[] = []; - - const fileAttachmentPromises = filesToConfirm.map(async (file) => { - try { - if (!file) { - console.warn('Skipping undefined file'); - return { files: [], attachments: [] }; - } - - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - return { files, attachments }; - } catch (error) { - console.error('Error processing file:', file, error); - return { files: [], attachments: [] }; - } - }); - - const filesAndAttachments = await Promise.all(fileAttachmentPromises); - - filesAndAttachments.forEach(({ files, attachments }) => { - messageFiles.push(...files); - messageAttachments.push(...attachments); - }); - - return { files: messageFiles, attachments: messageAttachments }; -}; - /** * Validates and sends the message object. */ @@ -272,7 +239,7 @@ export const sendMessage = async function ( }), ); if (message?.t !== 'e2e') { - const { files, attachments } = await parseFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); message.files = files; message.attachments = attachments; } From 65c7be7dfd69d20b7679c29e7110c1b0e0156386 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 31 Aug 2024 16:23:32 +0530 Subject: [PATCH 100/112] fix: maintain file upload order to ensure consistent fileId, fileUrl assignment, resolving encryption/decryption issues due to sequence discrepancies. --- apps/meteor/client/lib/chats/uploads.ts | 105 +++++++++++------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index ad29d514bc32..bf444690a144 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -58,11 +58,8 @@ const send = async ( }, ]); - const fileIds: string[] = []; - const fileUrls: string[] = []; - - files.forEach((f) => { - new Promise((resolve, reject) => { + const uploadPromises = files.map((f) => { + return new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { @@ -77,10 +74,6 @@ const send = async ( return; } const progress = (event.loaded / event.total) * 100; - if (progress === 100) { - resolve(); - } - updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { @@ -113,54 +106,10 @@ const send = async ( }, ); - xhr.onload = async () => { + xhr.onload = () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - fileIds.push(result.file._id); - fileUrls.push(result.file.url); - if (fileIds.length === files.length) { - if (msg === undefined) { - msg = ''; - } - - try { - let content; - if (getContent) { - content = await getContent(fileIds, fileUrls); - } - const text: IMessage = { - rid, - _id: id, - msg: msg || description || '', - ts: new Date(), - u: { _id: id, username: id }, - _updatedAt: new Date(), - tmid, - t, - content, - }; - await sdk.call('sendMessage', text, fileUrls, fileIds); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - } - } + resolve({ fileId: result.file._id, fileUrl: result.file.url }); } }; @@ -171,6 +120,52 @@ const send = async ( }); }); }); + + try { + const results = await Promise.all(uploadPromises); + const fileIds = results.map((result) => result.fileId); + const fileUrls = results.map((result) => result.fileUrl); + + if (msg === undefined) { + msg = ''; + } + + let content; + if (getContent) { + content = await getContent(fileIds, fileUrls); + } + const text: IMessage = { + rid, + _id: id, + msg: msg || description || '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), + tmid, + t, + content, + }; + await sdk.call('sendMessage', text, fileUrls, fileIds); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + } catch (error) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } }; export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ From c4fb1046cfbcfa5466dc30d42afe0bfb70ef1cd9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 31 Aug 2024 16:41:25 +0530 Subject: [PATCH 101/112] Added different function for file upload --- .../composer/messageBox/HandleFileUploads.ts | 193 ++++++++++++++++++ .../room/composer/messageBox/MessageBox.tsx | 182 +---------------- 2 files changed, 196 insertions(+), 179 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts new file mode 100644 index 000000000000..ef7bfc7f8c93 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -0,0 +1,193 @@ +import type { IMessage, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; + +import { e2e } from '../../../../../app/e2e/client'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; + +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; +const uploadFile = ( + file: File[] | File, + chat: any, + extraData?: Pick & { msg?: string }, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + setFilesToUpload?: (files: File[]) => void, +) => { + if (!chat) { + console.error('Chat context not found'); + return; + } + chat.uploads.send( + file, + { + ...extraData, + }, + getContent, + fileContent, + ); + chat.composer?.clear(); + setFilesToUpload?.([]); +}; + +const handleEncryptedFilesShared = async ( + filesToUpload: File[], + chat: any, + msg: string, + e2eRoom: any, + setFilesToUpload?: (files: File[]) => void, +) => { + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); + + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + + const imgDimensions = await Promise.all( + filesToUpload.map((file) => { + if (/^image\/.+/.test(file.type)) { + return getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + } + return null; + }), + ); + + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: filesToUpload[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = imgDimensions[i]; + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, + }); + } else if (/^video\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), + }); + } + + const files = { + _id: _id[i], + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: filesToUpload[0], + }); + }; + + const fileContentData = { + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; + uploadFile( + filesarray, + chat, + { + t: 'e2e', + }, + getContent, + uploadFileData, + setFilesToUpload, + ); + } +}; +export const handleSendFiles = async (filesToUpload: File[], chat: any, room: any, setFilesToUpload?: (files: File[]) => void) => { + if (!chat || !room) { + return; + } + + const msg = chat.composer?.text ?? ''; + + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(filesToUpload, chat, { msg }); + setFilesToUpload?.([]); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(filesToUpload, chat, { msg }); + setFilesToUpload?.([]); + return; + } + handleEncryptedFilesShared(filesToUpload, chat, msg, e2eRoom, setFilesToUpload); + chat.composer?.clear(); +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 63ad32d37214..f1a075710eff 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -21,12 +21,10 @@ import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; -import { e2e } from '../../../../../app/e2e/client'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime'; -import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; @@ -45,6 +43,7 @@ import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; import FilePreview from './FilePreview/FilePreview'; +import { handleSendFiles } from './HandleFileUploads'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; @@ -186,181 +185,6 @@ const MessageBox = ({ }, 300); }; - const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ - height: img.height, - width: img.width, - }); - }; - img.src = dataURL; - }); - }; - const uploadFile = ( - file: File[] | File, - extraData?: Pick & { msg?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ) => { - if (!chat) { - console.error('Chat context not found'); - return; - } - chat.uploads.send( - file, - { - ...extraData, - }, - getContent, - fileContent, - ); - chat.composer?.clear(); - setFilesToUpload([]); - }; - - const handleEncryptedFilesShared = async (filesToUpload: File[], msg: string, e2eRoom: any) => { - const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); - - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments: FileAttachmentProps[] = []; - const uploadFiles: any = []; - - const promises = _id.map(async (id, i) => { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - let updatedAttachment; - - if (/^image\/.+/.test(filesToUpload[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - updatedAttachment = { - ...attachment, - image_url: fileUrl[i], - image_type: filesToUpload[i].type, - image_size: filesToUpload[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }; - } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - updatedAttachment = { - ...attachment, - audio_url: fileUrl[i], - audio_type: filesToUpload[i].type, - audio_size: filesToUpload[i].size, - }; - } else if (/^video\/.+/.test(filesToUpload[i].type)) { - updatedAttachment = { - ...attachment, - video_url: fileUrl[i], - video_type: filesToUpload[i].type, - video_size: filesToUpload[i].size, - }; - } else { - updatedAttachment = { - ...attachment, - size: filesToUpload[i].size, - format: getFileExtension(filesToUpload[i].name), - }; - } - - attachments.push(updatedAttachment); - - const files = { - _id: id, - name: filesToUpload[i].name, - type: filesToUpload[i].type, - size: filesToUpload[i].size, - }; - - uploadFiles.push(files); - }); - await Promise.all(promises); - - return e2eRoom.encryptMessageContent({ - attachments, - files: uploadFiles, - file: filesToUpload[0], - }); - }; - - const fileContentData = { - type: filesToUpload[0].type, - typeGroup: filesToUpload[0].type.split('/')[0], - name: filesToUpload[0].name, - msg: msg || '', - encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, - }, - hashes: { - sha256: encryptedFilesarray[0].hash, - }, - }; - - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, - }; - uploadFile( - filesarray, - { - t: 'e2e', - }, - getContent, - uploadFileData, - ); - } - }; - const handleSendFiles = async (filesToUpload: File[]) => { - if (!chat || !room) { - return; - } - - const msg = chat.composer?.text ?? ''; - - filesToUpload.forEach((file) => { - Object.defineProperty(file, 'name', { - writable: true, - value: file.name, - }); - }); - - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(filesToUpload, { msg }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(filesToUpload, { msg }); - return; - } - handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); - chat.composer?.clear(); - setFilesToUpload([]); - }; - const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -404,7 +228,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (isUploading) { setIsUploading(!isUploading); - return handleSendFiles(filesToUpload); + return handleSendFiles(filesToUpload, chat, room, setFilesToUpload); } const text = chat.composer?.text ?? ''; chat.composer?.clear(); From 12abfc544046638487ee1e21b73329379d57c647 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 2 Sep 2024 09:58:59 +0530 Subject: [PATCH 102/112] removed unnecessary changes --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/client/lib/chats/uploads.ts | 2 +- .../meteor/client/views/room/composer/messageBox/MessageBox.tsx | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index b963bc8550be..0fa55d4dbcc1 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 630b2a39b27b..4b078c580bc3 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -174,7 +174,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, - description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index bf444690a144..f95dd8f9640c 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -147,7 +147,7 @@ const send = async ( }; await sdk.call('sendMessage', text, fileUrls, fileIds); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error) { + } catch (error: unknown) { updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f1a075710eff..359cc91ae9da 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -225,7 +225,7 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); - const handleSendMessage = useMutableCallback(async () => { + const handleSendMessage = useMutableCallback(() => { if (isUploading) { setIsUploading(!isUploading); return handleSendFiles(filesToUpload, chat, room, setFilesToUpload); From 8c91c8cdcc32c30769dea3b22ffe39069153fc5b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 3 Sep 2024 16:04:36 +0530 Subject: [PATCH 103/112] added file which upload the file and return back the file IDs and fileURLs --- .../lib/chats/uploadFilesAndGetIdsandURL.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts diff --git a/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts b/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts new file mode 100644 index 000000000000..eadf9072b058 --- /dev/null +++ b/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts @@ -0,0 +1,143 @@ +import type { IUpload } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import { Random } from '@rocket.chat/random'; + +import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { getErrorMessage } from '../errorHandling'; +import type { Upload } from './Upload'; + +let uploads: readonly Upload[] = []; + +const emitter = new Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }>(); + +const updateUploads = (update: (uploads: readonly Upload[]) => readonly Upload[]): void => { + uploads = update(uploads); + emitter.emit('update'); +}; +export const uploadAndGetIds = async ( + file: File, + { + rid, + tmid, + fileContent, + }: { + rid: string; + tmid?: string; + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }; + }, +): Promise<{ fileId: string; fileUrl: string }> => { + const id = Random.id(); + + updateUploads((uploads) => [ + ...uploads, + { + id, + name: fileContent?.raw.name || file.name, + percentage: 0, + }, + ]); + + try { + const result = await new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { + const xhr = sdk.rest.upload( + `/v1/rooms.media/${rid}`, + { + file, + ...(fileContent && { + content: JSON.stringify(fileContent.encrypted), + }), + }, + { + load: (event) => { + console.log('from uploadfiles event', event); + }, + progress: (event) => { + if (!event.lengthComputable) { + return; + } + const progress = (event.loaded / event.total) * 100; + if (progress === 100) { + return; + } + + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: Math.round(progress) || 0, + }; + }), + ); + }, + error: (event) => { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(xhr.responseText), + }; + }), + ); + reject(event); + }, + }, + ); + + xhr.onload = () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + + resolve({ + fileId: response.file._id, + fileUrl: response.file.url, + }); + } else { + reject(new Error('File upload failed.')); + } + }; + + if (uploads.length) { + UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + + emitter.once(`cancelling-${id}`, () => { + xhr.abort(); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + reject(new Error('Upload cancelled.')); + }); + }); + + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + + return result; + } catch (error: unknown) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + throw error; + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } +}; From 7e5081b3ad6bf58b15b50f968870b1d1abb85b50 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 6 Sep 2024 00:59:01 +0530 Subject: [PATCH 104/112] fix: added condition so it will remove extra space --- .../room/composer/messageBox/MessageBox.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 359cc91ae9da..9eb53f03a354 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -486,25 +486,29 @@ const MessageBox = ({ aria-activedescendant={ariaActiveDescendant} />
- - {filesToUpload.map((file, index) => ( -
+ - -
- ))} -
+ {filesToUpload.map((file, index) => ( +
+ +
+ ))} + + + )} Date: Sun, 8 Sep 2024 17:06:40 +0530 Subject: [PATCH 105/112] feat: Add drag-and-drop file upload to message box --- .../views/room/body/DropTargetOverlay.tsx | 7 +++---- apps/meteor/client/views/room/body/RoomBody.tsx | 7 +++++-- .../client/views/room/body/RoomBodyV2.tsx | 7 +++++-- .../views/room/composer/ComposerMessage.tsx | 3 ++- .../composer/messageBox/HandleFileUploads.ts | 17 ++++++++--------- .../room/composer/messageBox/MessageBox.tsx | 16 ++++++++++++---- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index 8a9b1ae77dbd..67b8ab436b91 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -9,13 +9,13 @@ import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; type DropTargetOverlayProps = { enabled: boolean; + setFilesToUplaod: any; reason?: ReactNode; - onFileDrop?: (files: File[]) => void; visible?: boolean; onDismiss?: () => void; }; -function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { +function DropTargetOverlay({ enabled, setFilesToUplaod, reason, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { const t = useTranslation(); const handleDragLeave = useMutableCallback((event: DragEvent) => { @@ -55,8 +55,7 @@ function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDism } } } - - onFileDrop?.(files); + setFilesToUplaod(files); }); if (!visible) { diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index a592bb1fa2c0..0a418b6e0ffe 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement, UIEvent } from 'react'; -import React, { memo, useCallback, useMemo, useRef } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { RoomRoles } from '../../../../app/models/client'; import { isTruthy } from '../../../../lib/isTruthy'; @@ -89,6 +89,7 @@ const RoomBody = (): ReactElement => { const useRealName = useSetting('UI_Use_Real_Name') as boolean; const innerBoxRef = useRef(null); + const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -225,7 +226,7 @@ const RoomBody = (): ReactElement => { >
- + {roomLeader ? ( { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index cfd6cb94cb51..e6a274179d83 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement } from 'react'; -import React, { memo, useCallback, useMemo, useRef } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; @@ -84,6 +84,7 @@ const RoomBody = (): ReactElement => { }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); const innerBoxRef = useRef(null); + const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -207,7 +208,7 @@ const RoomBody = (): ReactElement => { >
- +
{uploads.map((upload) => ( @@ -285,6 +286,8 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 790b1739bde7..e1666a796702 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -11,6 +11,8 @@ import ComposerSkeleton from './ComposerSkeleton'; import MessageBox from './messageBox/MessageBox'; export type ComposerMessageProps = { + filesToUpload: File[]; + setFilesToUpload: any; tmid?: IMessage['_id']; children?: ReactNode; subscription?: ISubscription; @@ -84,7 +86,6 @@ const ComposerMessage = ({ tmid, readOnly, onSend, ...props }: ComposerMessagePr const publicationReady = useReactiveValue( useCallback(() => LegacyRoomManager.getOpenedRoomByRid(room._id)?.streamActive ?? false, [room._id]), ); - if (!publicationReady) { return ; } diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts index ef7bfc7f8c93..e6d3dc92060c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -2,6 +2,7 @@ import type { IMessage, IUpload, IE2EEMessage, FileAttachmentProps } from '@rock import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; +import { prependReplies } from '../../../../lib/utils/prependReplies'; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { return new Promise((resolve) => { @@ -68,6 +69,7 @@ const handleEncryptedFilesShared = async ( title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], + description: i === 0 ? msg : undefined, title_link_download: true, encryption: { key: encryptedFilesarray[i].key, @@ -131,7 +133,6 @@ const handleEncryptedFilesShared = async ( type: filesToUpload[0].type, typeGroup: filesToUpload[0].type.split('/')[0], name: filesToUpload[0].name, - msg: msg || '', encryption: { key: encryptedFilesarray[0].key, iv: encryptedFilesarray[0].iv, @@ -141,11 +142,9 @@ const handleEncryptedFilesShared = async ( }, }; - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), }; uploadFile( filesarray, @@ -154,7 +153,7 @@ const handleEncryptedFilesShared = async ( t: 'e2e', }, getContent, - uploadFileData, + fileContent, setFilesToUpload, ); } @@ -163,8 +162,8 @@ export const handleSendFiles = async (filesToUpload: File[], chat: any, room: an if (!chat || !room) { return; } - - const msg = chat.composer?.text ?? ''; + const replies = chat.composer?.quotedMessages.get() ?? []; + const msg = await prependReplies(chat.composer?.text || '', replies); filesToUpload.forEach((file) => { Object.defineProperty(file, 'name', { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 9eb53f03a354..7db29a1b4329 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -17,7 +17,7 @@ import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessa import { useMutation } from '@tanstack/react-query'; import fileSize from 'filesize'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; -import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; +import React, { memo, useRef, useReducer, useCallback, useState, useEffect } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; @@ -82,6 +82,8 @@ const a: any[] = []; const getEmptyArray = () => a; type MessageBoxProps = { + filesToUpload: File[]; + setFilesToUpload: any; tmid?: IMessage['_id']; readOnly: boolean; onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise; @@ -102,6 +104,8 @@ type MessageBoxProps = { type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; const MessageBox = ({ + filesToUpload, + setFilesToUpload, tmid, onSend, onJoin, @@ -124,12 +128,16 @@ const MessageBox = ({ const [typing, setTyping] = useReducer(reducer, false); const [isUploading, setIsUploading] = useState(false); - const [filesToUpload, setFilesToUpload] = useState([]); + + useEffect(() => { + setIsUploading(filesToUpload.length > 0); + }, [filesToUpload]); + const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - const handleFilesToUpload: HandleFilesToUpload = (filesList, resetFileInput) => { - setFilesToUpload((prevFiles) => { + const handleFilesToUpload: HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => { + setFilesToUpload((prevFiles: File[]) => { let newFilesToUpload = [...prevFiles, ...filesList]; if (newFilesToUpload.length > 6) { newFilesToUpload = newFilesToUpload.slice(0, 6); From d21dd7f5c2b5a9cfcf1ee18fa03381de4077aab2 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 11 Sep 2024 11:01:24 +0530 Subject: [PATCH 106/112] fix: added encryption of message also --- .../client/views/room/composer/messageBox/HandleFileUploads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts index e6d3dc92060c..5cff06239d33 100644 --- a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -69,7 +69,6 @@ const handleEncryptedFilesShared = async ( title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], - description: i === 0 ? msg : undefined, title_link_download: true, encryption: { key: encryptedFilesarray[i].key, @@ -126,6 +125,7 @@ const handleEncryptedFilesShared = async ( attachments, files: arrayoffiles, file: filesToUpload[0], + msg, }); }; From 8508954a66953b51c978f250ff2e2c625f5d5302 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 20 Sep 2024 11:59:48 +0530 Subject: [PATCH 107/112] fix: Issue inside the thread messages --- .../room/contextualBar/Threads/components/ThreadChat.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx index 7afe0b442bbc..14b024014afa 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -24,6 +24,7 @@ type ThreadChatProps = { const ThreadChat = ({ mainMessage }: ThreadChatProps) => { const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(); + const [filesToUpload, setFilesToUpload] = useState([]); const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel'); @@ -95,7 +96,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { return ( - + { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} tshow={sendToChannel} > From ea689a23738e77eb199ee3499611d3ecfab08805 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 20 Sep 2024 12:01:49 +0530 Subject: [PATCH 108/112] fix: multiple dispatch messages --- .../room/composer/messageBox/MessageBox.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 7db29a1b4329..b8207a867018 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -146,29 +146,39 @@ const MessageBox = ({ message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", }); } + let nameError = 0; + let sizeError = 0; const validFiles = newFilesToUpload.filter((queuedFile) => { const { name, size } = queuedFile; if (!name) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); + nameError = 1; return false; } if (maxFileSize > -1 && (size || 0) > maxFileSize) { - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - }); + sizeError = 1; return false; } return true; }); + if (nameError) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + } + + if (sizeError) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + } + setIsUploading(validFiles.length > 0); return validFiles; }); From acdf3c1e4428c218251202db6ccf6f4774415900 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 22 Sep 2024 00:28:44 +0530 Subject: [PATCH 109/112] removed onFileDrop as using setFilesToUpload --- apps/meteor/client/views/room/body/DropTargetOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index be60a39db86f..bcb57c407c80 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -19,7 +19,7 @@ function DropTargetOverlay({ enabled, setFilesToUplaod, reason, - onFileDrop, + // onFileDrop, // not using onFileDrop anymore as we use setFilesToUplaod visible = true, onDismiss, }: DropTargetOverlayProps): ReactElement | null { From 5f223159121ebcb380868bfbd1677443069cf409 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 6 Oct 2024 01:05:59 +0530 Subject: [PATCH 110/112] fix: remove selected files when in edit mode --- .../views/room/composer/messageBox/MessageBox.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index b8207a867018..e007ff240d00 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -129,10 +129,6 @@ const MessageBox = ({ const [typing, setTyping] = useReducer(reducer, false); const [isUploading, setIsUploading] = useState(false); - useEffect(() => { - setIsUploading(filesToUpload.length > 0); - }, [filesToUpload]); - const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; @@ -345,6 +341,13 @@ const MessageBox = ({ subscribe: chat.composer?.editing.subscribe ?? emptySubscribe, }); + useEffect(() => { + setIsUploading(filesToUpload.length > 0); + if (isEditing) { + setFilesToUpload([]); + } + }, [filesToUpload, isEditing, setFilesToUpload]); + const isRecordingAudio = useSubscription({ getCurrentValue: chat.composer?.recording.get ?? getEmptyFalse, subscribe: chat.composer?.recording.subscribe ?? emptySubscribe, From 19df253cbd0ac6e6b2d03eeb3c3cb0851c6e2af3 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 6 Oct 2024 21:05:58 +0530 Subject: [PATCH 111/112] fix: diabled file sharing while editing --- .../client/views/room/composer/messageBox/MessageBox.tsx | 1 + .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index e007ff240d00..c356d5d5e0bc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -555,6 +555,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} + isEditing={isEditing} handleFiles={handleFilesToUpload} /> diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 102be2a17945..253539930d5a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,6 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; + isEditing: boolean; handleFiles: (filesList: File[], resetFileInput?: () => void) => void; }; @@ -46,6 +47,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + isEditing = false, handleFiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); @@ -59,7 +61,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || isRecording, handleFiles); + const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); From ec6a12feea64c49894d086fe4fce2aff322fb655 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 9 Oct 2024 17:53:54 +0530 Subject: [PATCH 112/112] fix:TS_error_resolved --- apps/meteor/client/views/room/body/DropTargetOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index ec16d8b61517..21b2c0af6609 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -23,7 +23,7 @@ function DropTargetOverlay({ visible = true, onDismiss, }: DropTargetOverlayProps): ReactElement | null { - const { t } = useTranslation(); + const t = useTranslation(); const handleDragLeave = useMutableCallback((event: DragEvent) => { event.stopPropagation();