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