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