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.
		
		
		
		
		
			
		
			
				
	
	
		
			150 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			150 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
| import Queue from 'queue-promise';
 | |
| import ByteBuffer from 'bytebuffer';
 | |
| import _ from 'lodash';
 | |
| 
 | |
| import { downloadAttachment } from './attachments';
 | |
| 
 | |
| import { allowOnlyOneAtATime, hasAlreadyOneAtaTimeMatching } from '../session/utils/Promise';
 | |
| import { toHex } from '../session/utils/String';
 | |
| import { processNewAttachment } from '../types/MessageAttachment';
 | |
| import { MIME } from '../types';
 | |
| import { autoScaleForIncomingAvatar } from '../util/attachmentsUtil';
 | |
| import { decryptProfile } from '../util/crypto/profileEncrypter';
 | |
| import { ConversationModel } from '../models/conversation';
 | |
| import { SignalService } from '../protobuf';
 | |
| import { getConversationController } from '../session/conversations';
 | |
| import { UserUtils } from '../session/utils';
 | |
| 
 | |
| const queue = new Queue({
 | |
|   concurrent: 1,
 | |
|   interval: 500,
 | |
| });
 | |
| 
 | |
| queue.on('reject', error => {
 | |
|   window.log.warn('[profileupdate] task profile image update failed with', error);
 | |
| });
 | |
| 
 | |
| export async function appendFetchAvatarAndProfileJob(
 | |
|   conversation: ConversationModel,
 | |
|   profileInDataMessage: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null // was any
 | |
| ) {
 | |
|   if (!conversation?.id) {
 | |
|     window?.log?.warn('[profileupdate] Cannot update profile with empty convoid');
 | |
|     return;
 | |
|   }
 | |
|   const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversation.id}`;
 | |
| 
 | |
|   if (hasAlreadyOneAtaTimeMatching(oneAtaTimeStr)) {
 | |
|     // window.log.debug(
 | |
|     //   '[profileupdate] not adding another task of "appendFetchAvatarAndProfileJob" as there is already one scheduled for the conversation: ',
 | |
|     //   conversation.id
 | |
|     // );
 | |
|     return;
 | |
|   }
 | |
|   // window.log.info(`[profileupdate] queuing fetching avatar for ${conversation.id}`);
 | |
|   const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | |
|     return createOrUpdateProfile(conversation, profileInDataMessage, profileKey);
 | |
|   });
 | |
| 
 | |
|   queue.enqueue(async () => task);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This function should be used only when we have to do a sync update to our conversation with a new profile/avatar image or display name
 | |
|  * It tries to fetch the profile image, scale it, save it, and update the conversationModel
 | |
|  */
 | |
| export async function updateOurProfileSync(
 | |
|   profileInDataMessage: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null // was any
 | |
| ) {
 | |
|   const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
 | |
|   if (!ourConvo?.id) {
 | |
|     window?.log?.warn('[profileupdate] Cannot update our profile with empty convoid');
 | |
|     return;
 | |
|   }
 | |
|   const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${ourConvo.id}`;
 | |
|   return allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | |
|     return createOrUpdateProfile(ourConvo, profileInDataMessage, profileKey);
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Creates a new profile from the profile provided. Creates the profile if it doesn't exist.
 | |
|  */
 | |
| async function createOrUpdateProfile(
 | |
|   conversation: ConversationModel,
 | |
|   profileInDataMessage: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null
 | |
| ) {
 | |
|   if (!conversation.isPrivate()) {
 | |
|     window.log.warn('createOrUpdateProfile can only be used for private convos');
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const existingDisplayName = conversation.get('displayNameInProfile');
 | |
|   const newDisplayName = profileInDataMessage.displayName;
 | |
| 
 | |
|   let changes = false;
 | |
|   if (existingDisplayName !== newDisplayName) {
 | |
|     changes = true;
 | |
|     conversation.set('displayNameInProfile', newDisplayName || undefined);
 | |
|   }
 | |
| 
 | |
|   if (profileInDataMessage.profilePicture && profileKey) {
 | |
|     const prevPointer = conversation.get('avatarPointer');
 | |
|     const needsUpdate =
 | |
|       !prevPointer || !_.isEqual(prevPointer, profileInDataMessage.profilePicture);
 | |
| 
 | |
|     if (needsUpdate) {
 | |
|       try {
 | |
|         window.log.debug(`[profileupdate] starting downloading task for  ${conversation.id}`);
 | |
|         const downloaded = await downloadAttachment({
 | |
|           url: profileInDataMessage.profilePicture,
 | |
|           isRaw: true,
 | |
|         });
 | |
| 
 | |
|         // null => use placeholder with color and first letter
 | |
|         let path = null;
 | |
|         if (profileKey) {
 | |
|           // Convert profileKey to ArrayBuffer, if needed
 | |
|           const encoding = typeof profileKey === 'string' ? 'base64' : undefined;
 | |
|           try {
 | |
|             const profileKeyArrayBuffer = ByteBuffer.wrap(profileKey, encoding).toArrayBuffer();
 | |
|             const decryptedData = await decryptProfile(downloaded.data, profileKeyArrayBuffer);
 | |
|             window.log.info(
 | |
|               `[profileupdate] about to auto scale avatar for convo ${conversation.id}`
 | |
|             );
 | |
| 
 | |
|             const scaledData = await autoScaleForIncomingAvatar(decryptedData);
 | |
|             const upgraded = await processNewAttachment({
 | |
|               data: await scaledData.blob.arrayBuffer(),
 | |
|               contentType: MIME.IMAGE_UNKNOWN, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
 | |
|             });
 | |
|             // Only update the convo if the download and decrypt is a success
 | |
|             conversation.set('avatarPointer', profileInDataMessage.profilePicture);
 | |
|             conversation.set('profileKey', toHex(profileKey));
 | |
|             ({ path } = upgraded);
 | |
|           } catch (e) {
 | |
|             window?.log?.error(`[profileupdate] Could not decrypt profile image: ${e}`);
 | |
|           }
 | |
|         }
 | |
|         conversation.set({ avatarInProfile: path || undefined });
 | |
| 
 | |
|         changes = true;
 | |
|       } catch (e) {
 | |
|         window.log.warn(
 | |
|           `[profileupdate] Failed to download attachment at ${profileInDataMessage.profilePicture}. Maybe it expired? ${e.message}`
 | |
|         );
 | |
|         // do not return here, we still want to update the display name even if the avatar failed to download
 | |
|       }
 | |
|     }
 | |
|   } else if (profileKey) {
 | |
|     conversation.set({ avatarInProfile: undefined });
 | |
|   }
 | |
| 
 | |
|   if (changes) {
 | |
|     await conversation.commit();
 | |
|   }
 | |
| }
 |