You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			264 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			264 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			TypeScript
		
	
| import { omit, startsWith } from 'lodash';
 | |
| 
 | |
| import { MessageModel } from '../models/message';
 | |
| import { Data } from '../../ts/data/data';
 | |
| import { AttachmentDownloads } from '../session/utils';
 | |
| import { ConversationModel } from '../models/conversation';
 | |
| import { OpenGroupRequestCommonType } from '../session/apis/open_group_api/opengroupV2/ApiUtil';
 | |
| import { getUnpaddedAttachment } from '../session/crypto/BufferPadding';
 | |
| import { decryptAttachment } from '../util/crypto/attachmentsEncrypter';
 | |
| import { callUtilsWorker } from '../webworker/workers/browser/util_worker_interface';
 | |
| import { sogsV3FetchFileByFileID } from '../session/apis/open_group_api/sogsv3/sogsV3FetchFile';
 | |
| import { OpenGroupData } from '../data/opengroups';
 | |
| import {
 | |
|   downloadFileFromFileServer,
 | |
|   fileServerURL,
 | |
| } from '../session/apis/file_server_api/FileServerApi';
 | |
| 
 | |
| export async function downloadAttachment(attachment: {
 | |
|   url: string;
 | |
|   id?: string;
 | |
|   isRaw?: boolean;
 | |
|   key?: string;
 | |
|   digest?: string;
 | |
|   size?: number;
 | |
| }) {
 | |
|   const asURL = new URL(attachment.url);
 | |
|   const serverUrl = asURL.origin;
 | |
| 
 | |
|   // is it an attachment hosted on the file server
 | |
|   const defaultFileServer = startsWith(serverUrl, fileServerURL);
 | |
| 
 | |
|   let res: ArrayBuffer | null = null;
 | |
| 
 | |
|   if (defaultFileServer) {
 | |
|     let attachmentId = attachment.id;
 | |
|     if (!attachmentId) {
 | |
|       // try to get the fileId from the end of the URL
 | |
|       attachmentId = attachment.url;
 | |
|     }
 | |
|     window?.log?.info('Download v2 file server attachment', attachmentId);
 | |
|     res = await downloadFileFromFileServer(attachmentId);
 | |
|   } else {
 | |
|     window.log.warn(
 | |
|       `downloadAttachment attachment is neither opengroup attachment nor fileserver... Dropping it ${asURL.href}`
 | |
|     );
 | |
|     throw new Error('Attachment url is not opengroupv2 nor fileserver. Unsupported');
 | |
|   }
 | |
| 
 | |
|   if (!res?.byteLength) {
 | |
|     window?.log?.error('Failed to download attachment. Length is 0');
 | |
|     throw new Error(`Failed to download attachment. Length is 0 for ${attachment.url}`);
 | |
|   }
 | |
| 
 | |
|   // The attachment id is actually just the absolute url of the attachment
 | |
|   let data = res;
 | |
|   if (!attachment.isRaw) {
 | |
|     const { key, digest, size } = attachment;
 | |
| 
 | |
|     if (!key || !digest) {
 | |
|       throw new Error('Attachment is not raw but we do not have a key to decode it');
 | |
|     }
 | |
|     if (!size) {
 | |
|       throw new Error('Attachment expected size is 0');
 | |
|     }
 | |
| 
 | |
|     const keyBuffer = (await callUtilsWorker('fromBase64ToArrayBuffer', key)) as ArrayBuffer;
 | |
|     const digestBuffer = (await callUtilsWorker('fromBase64ToArrayBuffer', digest)) as ArrayBuffer;
 | |
| 
 | |
|     data = await decryptAttachment(data, keyBuffer, digestBuffer);
 | |
| 
 | |
|     if (size !== data.byteLength) {
 | |
|       // we might have padding, check that all the remaining bytes are padding bytes
 | |
|       // otherwise we have an error.
 | |
|       const unpaddedData = getUnpaddedAttachment(data, size);
 | |
|       if (!unpaddedData) {
 | |
|         throw new Error(
 | |
|           `downloadAttachment: Size ${size} did not match downloaded attachment size ${data.byteLength}`
 | |
|         );
 | |
|       }
 | |
|       data = unpaddedData;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     ...omit(attachment, 'digest', 'key'),
 | |
|     data,
 | |
|   };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * Download the attachment based on the url.
 | |
|  * The only time where the size should be set to null, is when downloading the image for a sogs room (as we do not have the size for it).
 | |
|  *
 | |
|  * @param attachment Either the details of the attachment to download (on a per room basis), or the pathName to the file you want to get
 | |
|  */
 | |
| export async function downloadAttachmentSogsV3(
 | |
|   attachment: {
 | |
|     id: number;
 | |
|     url: string;
 | |
|     size: number | null;
 | |
|   },
 | |
|   roomInfos: OpenGroupRequestCommonType
 | |
| ) {
 | |
|   const roomDetails = OpenGroupData.getV2OpenGroupRoomByRoomId(roomInfos);
 | |
|   if (!roomDetails) {
 | |
|     throw new Error(`Didn't find such a room ${roomInfos.serverUrl}: ${roomInfos.roomId}`);
 | |
|   }
 | |
| 
 | |
|   const dataUint = await sogsV3FetchFileByFileID(roomDetails, `${attachment.id}`);
 | |
| 
 | |
|   if (!dataUint?.length) {
 | |
|     window?.log?.error('Failed to download attachment. Length is 0');
 | |
|     throw new Error(`Failed to download attachment. Length is 0 for ${attachment.url}`);
 | |
|   }
 | |
| 
 | |
|   if (attachment.size === null) {
 | |
|     return {
 | |
|       ...omit(attachment, 'digest', 'key'),
 | |
|       data: dataUint.buffer,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   let data = dataUint;
 | |
|   if (attachment.size !== dataUint.length) {
 | |
|     // we might have padding, check that all the remaining bytes are padding bytes
 | |
|     // otherwise we have an error.
 | |
|     const unpaddedData = getUnpaddedAttachment(dataUint.buffer, attachment.size);
 | |
|     if (!unpaddedData) {
 | |
|       throw new Error(
 | |
|         `downloadAttachment: Size ${attachment.size} did not match downloaded attachment size ${data.byteLength}`
 | |
|       );
 | |
|     }
 | |
|     data = new Uint8Array(unpaddedData);
 | |
|   } else {
 | |
|     // nothing to do, the attachment has already the correct size.
 | |
|     // There is just no padding included, which is what we agreed on
 | |
|     // window?.log?.info('Received opengroupv2 unpadded attachment size:', attachment.size);
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     ...omit(attachment, 'digest', 'key'),
 | |
|     data: data.buffer,
 | |
|   };
 | |
| }
 | |
| 
 | |
| async function processNormalAttachments(
 | |
|   message: MessageModel,
 | |
|   normalAttachments: Array<any>,
 | |
|   convo: ConversationModel
 | |
| ): Promise<number> {
 | |
|   const isOpenGroupV2 = convo.isOpenGroupV2();
 | |
| 
 | |
|   if (message.isTrustedForAttachmentDownload()) {
 | |
|     const openGroupV2Details = (isOpenGroupV2 && convo.toOpenGroupV2()) || undefined;
 | |
|     const attachments = await Promise.all(
 | |
|       normalAttachments.map(async (attachment: any, index: number) => {
 | |
|         return AttachmentDownloads.addJob(attachment, {
 | |
|           messageId: message.id,
 | |
|           type: 'attachment',
 | |
|           index,
 | |
|           isOpenGroupV2,
 | |
|           openGroupV2Details,
 | |
|         });
 | |
|       })
 | |
|     );
 | |
| 
 | |
|     message.set({ attachments });
 | |
| 
 | |
|     return attachments.length;
 | |
|   }
 | |
|   window.log.info('No downloading attachments yet as this user is not trusted for now.');
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| async function processPreviews(message: MessageModel, convo: ConversationModel): Promise<number> {
 | |
|   let addedCount = 0;
 | |
|   const isOpenGroupV2 = convo.isOpenGroupV2();
 | |
|   const openGroupV2Details = (isOpenGroupV2 && convo.toOpenGroupV2()) || undefined;
 | |
| 
 | |
|   const preview = await Promise.all(
 | |
|     (message.get('preview') || []).map(async (item: any, index: number) => {
 | |
|       if (!item.image) {
 | |
|         return item;
 | |
|       }
 | |
|       addedCount += 1;
 | |
| 
 | |
|       const image = message.isTrustedForAttachmentDownload()
 | |
|         ? await AttachmentDownloads.addJob(item.image, {
 | |
|             messageId: message.id,
 | |
|             type: 'preview',
 | |
|             index,
 | |
|             isOpenGroupV2,
 | |
|             openGroupV2Details,
 | |
|           })
 | |
|         : null;
 | |
| 
 | |
|       return { ...item, image };
 | |
|     })
 | |
|   );
 | |
| 
 | |
|   message.set({ preview });
 | |
| 
 | |
|   return addedCount;
 | |
| }
 | |
| 
 | |
| async function processQuoteAttachments(
 | |
|   message: MessageModel,
 | |
|   convo: ConversationModel
 | |
| ): Promise<number> {
 | |
|   let addedCount = 0;
 | |
| 
 | |
|   const quote = message.get('quote');
 | |
| 
 | |
|   if (!quote || !quote.attachments || !quote.attachments.length) {
 | |
|     return 0;
 | |
|   }
 | |
|   const isOpenGroupV2 = convo.isOpenGroupV2();
 | |
|   const openGroupV2Details = (isOpenGroupV2 && convo.toOpenGroupV2()) || undefined;
 | |
| 
 | |
|   quote.attachments = await Promise.all(
 | |
|     quote.attachments.map(async (item: any, index: any) => {
 | |
|       // If we already have a path, then we copied this image from the quoted
 | |
|       //    message and we don't need to download the attachment.
 | |
|       if (!item.thumbnail || item.thumbnail.path) {
 | |
|         return item;
 | |
|       }
 | |
| 
 | |
|       addedCount += 1;
 | |
| 
 | |
|       const thumbnail = await AttachmentDownloads.addJob(item.thumbnail, {
 | |
|         messageId: message.id,
 | |
|         type: 'quote',
 | |
|         index,
 | |
|         isOpenGroupV2,
 | |
|         openGroupV2Details,
 | |
|       });
 | |
| 
 | |
|       return { ...item, thumbnail };
 | |
|     })
 | |
|   );
 | |
| 
 | |
|   message.set({ quote });
 | |
| 
 | |
|   return addedCount;
 | |
| }
 | |
| 
 | |
| export async function queueAttachmentDownloads(
 | |
|   message: MessageModel,
 | |
|   conversation: ConversationModel
 | |
| ): Promise<void> {
 | |
|   let count = 0;
 | |
| 
 | |
|   count += await processNormalAttachments(message, message.get('attachments') || [], conversation);
 | |
| 
 | |
|   count += await processPreviews(message, conversation);
 | |
| 
 | |
|   count += await processQuoteAttachments(message, conversation);
 | |
| 
 | |
|   if (count > 0) {
 | |
|     await Data.saveMessage(message.attributes);
 | |
|   }
 | |
| }
 |