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.
		
		
		
		
		
			
		
			
				
	
	
		
			151 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			151 lines
		
	
	
		
			5.6 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, ConversationTypeEnum } 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('[profile-update] task profile image update failed with', error);
 | |
| });
 | |
| 
 | |
| export async function appendFetchAvatarAndProfileJob(
 | |
|   conversation: ConversationModel,
 | |
|   profile: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null // was any
 | |
| ) {
 | |
|   if (!conversation?.id) {
 | |
|     window?.log?.warn('[profile-update] Cannot update profile with empty convoid');
 | |
|     return;
 | |
|   }
 | |
|   const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${conversation.id}`;
 | |
| 
 | |
|   if (hasAlreadyOneAtaTimeMatching(oneAtaTimeStr)) {
 | |
|     // window.log.debug(
 | |
|     //   '[profile-update] not adding another task of "appendFetchAvatarAndProfileJob" as there is already one scheduled for the conversation: ',
 | |
|     //   conversation.id
 | |
|     // );
 | |
|     return;
 | |
|   }
 | |
|   window.log.info(`[profile-update] queuing fetching avatar for ${conversation.id}`);
 | |
|   const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | |
|     return createOrUpdateProfile(conversation, profile, 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(
 | |
|   profile: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null // was any
 | |
| ) {
 | |
|   const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
 | |
|   if (!ourConvo?.id) {
 | |
|     window?.log?.warn('[profile-update] Cannot update our profile with empty convoid');
 | |
|     return;
 | |
|   }
 | |
|   const oneAtaTimeStr = `appendFetchAvatarAndProfileJob:${ourConvo.id}`;
 | |
|   return allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | |
|     return createOrUpdateProfile(ourConvo, profile, profileKey);
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Creates a new profile from the profile provided. Creates the profile if it doesn't exist.
 | |
|  */
 | |
| async function createOrUpdateProfile(
 | |
|   conversation: ConversationModel,
 | |
|   profile: SignalService.DataMessage.ILokiProfile,
 | |
|   profileKey?: Uint8Array | null
 | |
| ) {
 | |
|   // Retain old values unless changed:
 | |
|   const newProfile = conversation.get('profile') || {};
 | |
| 
 | |
|   let changes = false;
 | |
|   if (newProfile.displayName !== profile.displayName) {
 | |
|     changes = true;
 | |
|   }
 | |
|   newProfile.displayName = profile.displayName;
 | |
| 
 | |
|   if (profile.profilePicture && profileKey) {
 | |
|     const prevPointer = conversation.get('avatarPointer');
 | |
|     const needsUpdate = !prevPointer || !_.isEqual(prevPointer, profile.profilePicture);
 | |
| 
 | |
|     if (needsUpdate) {
 | |
|       try {
 | |
|         window.log.debug(`[profile-update] starting downloading task for  ${conversation.id}`);
 | |
|         const downloaded = await downloadAttachment({
 | |
|           url: profile.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(
 | |
|               `[profile-update] 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', profile.profilePicture);
 | |
|             conversation.set('profileKey', toHex(profileKey));
 | |
|             ({ path } = upgraded);
 | |
|           } catch (e) {
 | |
|             window?.log?.error(`[profile-update] Could not decrypt profile image: ${e}`);
 | |
|           }
 | |
|         }
 | |
|         newProfile.avatar = path;
 | |
|         changes = true;
 | |
|       } catch (e) {
 | |
|         window.log.warn(
 | |
|           `[profile-update] Failed to download attachment at ${profile.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) {
 | |
|     if (newProfile.avatar !== null) {
 | |
|       changes = true;
 | |
|     }
 | |
|     newProfile.avatar = null;
 | |
|   }
 | |
| 
 | |
|   const conv = await getConversationController().getOrCreateAndWait(
 | |
|     conversation.id,
 | |
|     ConversationTypeEnum.PRIVATE
 | |
|   );
 | |
|   await conv.setLokiProfile(newProfile);
 | |
|   if (changes) {
 | |
|     await conv.commit();
 | |
|   }
 | |
| }
 |