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();
 | 
						|
  }
 | 
						|
}
 |