fix: make avatar download job only take conversationId

and not the profileKey+url too as we need the db entry to be in sync
with the config wrapper data (otherwise the next commit would be made
with data out of date from the wrapper side)
pull/2756/head
Audric Ackermann 2 years ago
parent c42f828044
commit 0cbcecb508

@ -128,7 +128,7 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str
}, 60000); }, 60000);
if (displayNameFromNetwork.length) { if (displayNameFromNetwork.length) {
// display name, avatars, groups and contacts should already be handled when this event was triggered. // display name, avatars, groups and contacts should already be handled when this event was triggered.
window?.log?.info('We got a displayName from network: '); window?.log?.info(`We got a displayName from network: "${displayNameFromNetwork}"`);
} else { } else {
window?.log?.info('Got a config message from network but without a displayName...'); window?.log?.info('Got a config message from network but without a displayName...');
throw new Error('Got a config message from network but without a displayName...'); throw new Error('Got a config message from network but without a displayName...');

@ -19,7 +19,7 @@ import { getConversationController } from '../session/conversations';
import { IncomingMessage } from '../session/messages/incoming/IncomingMessage'; import { IncomingMessage } from '../session/messages/incoming/IncomingMessage';
import { ProfileManager } from '../session/profile_manager/ProfileManager'; import { ProfileManager } from '../session/profile_manager/ProfileManager';
import { PubKey } from '../session/types'; import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils'; import { StringUtils, UserUtils } from '../session/utils';
import { toHex } from '../session/utils/String'; import { toHex } from '../session/utils/String';
import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob'; import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob';
import { IncomingConfResult, LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { IncomingConfResult, LibSessionUtil } from '../session/utils/libsession/libsession_utils';
@ -53,6 +53,8 @@ import { queueAllCachedFromSource } from './receiver';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../interactions/conversationInteractions'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../interactions/conversationInteractions';
const printDumpsForDebugging = false;
function groupByVariant( function groupByVariant(
incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>> incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
) { ) {
@ -97,6 +99,12 @@ async function mergeConfigsWithIncomingUpdates(
data: msg.message.data, data: msg.message.data,
hash: msg.messageHash, hash: msg.messageHash,
})); }));
if (printDumpsForDebugging) {
window.log.info(
`printDumpsForDebugging: before merge of ${variant}:`,
StringUtils.toHex(await GenericWrapperActions.dump(variant))
);
}
const mergedCount = await GenericWrapperActions.merge(variant, toMerge); const mergedCount = await GenericWrapperActions.merge(variant, toMerge);
const needsPush = await GenericWrapperActions.needsPush(variant); const needsPush = await GenericWrapperActions.needsPush(variant);
const needsDump = await GenericWrapperActions.needsDump(variant); const needsDump = await GenericWrapperActions.needsDump(variant);
@ -106,6 +114,12 @@ async function mergeConfigsWithIncomingUpdates(
`${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} ` `${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} `
); );
if (printDumpsForDebugging) {
window.log.info(
`printDumpsForDebugging: after merge of ${variant}:`,
StringUtils.toHex(await GenericWrapperActions.dump(variant))
);
}
const incomingConfResult: IncomingConfResult = { const incomingConfResult: IncomingConfResult = {
needsDump, needsDump,
needsPush, needsPush,

@ -1,8 +1,8 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { getConversationController } from '../conversations'; import { getConversationController } from '../conversations';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { AvatarDownload } from '../utils/job_runners/jobs/AvatarDownloadJob';
import { toHex } from '../utils/String'; import { toHex } from '../utils/String';
import { AvatarDownload } from '../utils/job_runners/jobs/AvatarDownloadJob';
/** /**
* This can be used to update our conversation display name with the given name right away, and plan an AvatarDownloadJob to retrieve the new avatar if needed to download it * This can be used to update our conversation display name with the given name right away, and plan an AvatarDownloadJob to retrieve the new avatar if needed to download it
@ -42,21 +42,45 @@ async function updateProfileOfContact(
window.log.warn('updateProfileOfContact can only be used for existing and private convos'); window.log.warn('updateProfileOfContact can only be used for existing and private convos');
return; return;
} }
let changes = false;
const existingDisplayName = conversation.get('displayNameInProfile'); const existingDisplayName = conversation.get('displayNameInProfile');
// avoid setting the display name to an invalid value // avoid setting the display name to an invalid value
if (existingDisplayName !== displayName && !isEmpty(displayName)) { if (existingDisplayName !== displayName && !isEmpty(displayName)) {
conversation.set('displayNameInProfile', displayName || undefined); conversation.set('displayNameInProfile', displayName || undefined);
await conversation.commit(); changes = true;
} }
// add an avatar download job only if needed
const profileKeyHex = !profileKey || isEmpty(profileKey) ? null : toHex(profileKey); const profileKeyHex = !profileKey || isEmpty(profileKey) ? null : toHex(profileKey);
await AvatarDownload.addAvatarDownloadJobIfNeeded({
profileKeyHex, let avatarChanged = false;
profileUrl, // trust whatever we get as an update. It either comes from a shared config wrapper or one of that user's message. But in any case we should trust it, even if it gets resetted.
pubkey, const prevPointer = conversation.get('avatarPointer');
}); const prevProfileKey = conversation.get('profileKey');
// we have to set it right away and not in the async download job, as the next .commit will save it to the
// database and wrapper (and we do not want to override anything in the wrapper's content
// with what we have locally, so we need the commit to have already the right values in pointer and profileKey)
if (prevPointer !== profileUrl || prevProfileKey !== profileKeyHex) {
conversation.set({
avatarPointer: profileUrl || undefined,
profileKey: profileKeyHex || undefined,
});
// if the avatar data we had before is not the same of what we received, we need to schedule a new avatar download job.
avatarChanged = true; // allow changes from strings to null/undefined to trigger a AvatarDownloadJob. If that happens, we want to remove the local attachment file.
}
if (changes) {
await conversation.commit();
}
if (avatarChanged) {
// this call will download the new avatar or reset the local filepath if needed
await AvatarDownload.addAvatarDownloadJob({
conversationId: pubkey,
});
}
} }
export const ProfileManager = { export const ProfileManager = {

@ -98,16 +98,6 @@ export class PersistedJobRunner<T extends TypeOfPersistedData> {
return 'type_exists'; return 'type_exists';
} }
// if addJobCheck returned 'removeJobsFromQueue it means that job logic estimates some jobs have to remove before adding that one.
// so let's grab the jobs to remove, remove them, and then add that new job nevertheless
if (addJobChecks === 'removeJobsFromQueue') {
// fetch all the jobs which we should remove and remove them
const toRemove = job.nonRunningJobsToRemove(serializedNonRunningJobs);
this.deleteJobsByIdentifier(toRemove.map(m => m.identifier));
this.sortJobsList();
await this.writeJobsToDB();
}
// make sure there is no job with that same identifier already . // make sure there is no job with that same identifier already .
window.log.info(`job runner adding type :"${job.persistedData.jobType}" `); window.log.info(`job runner adding type :"${job.persistedData.jobType}" `);

@ -29,8 +29,6 @@ export interface FakeSleepForMultiJobData extends PersistedJobData {
export interface AvatarDownloadPersistedData extends PersistedJobData { export interface AvatarDownloadPersistedData extends PersistedJobData {
jobType: 'AvatarDownloadJobType'; jobType: 'AvatarDownloadJobType';
conversationId: string; conversationId: string;
profileKeyHex: string | null;
profilePictureUrl: string | null;
} }
export interface ConfigurationSyncPersistedData extends PersistedJobData { export interface ConfigurationSyncPersistedData extends PersistedJobData {
@ -43,11 +41,7 @@ export type TypeOfPersistedData =
| FakeSleepJobData | FakeSleepJobData
| FakeSleepForMultiJobData; | FakeSleepForMultiJobData;
export type AddJobCheckReturn = export type AddJobCheckReturn = 'skipAddSameJobPresent' | 'sameJobDataAlreadyInQueue' | null;
| 'skipAddSameJobPresent'
| 'removeJobsFromQueue'
| 'sameJobDataAlreadyInQueue'
| null;
export enum RunJobResult { export enum RunJobResult {
Success = 1, Success = 1,

@ -1,4 +1,4 @@
import { isEmpty, isEqual, isNumber, isString } from 'lodash'; import { isEmpty, isNumber, isString } from 'lodash';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { UserUtils } from '../..'; import { UserUtils } from '../..';
import { downloadAttachment } from '../../../../receiver/attachments'; import { downloadAttachment } from '../../../../receiver/attachments';
@ -20,18 +20,11 @@ const defaultMsBetweenRetries = 10000;
const defaultMaxAttemps = 3; const defaultMaxAttemps = 3;
/** /**
* Returns true if given those details we should add an Avatar Download Job to the list of jobs to run * Returns true if the provided conversationId is a private chat and that we should add an Avatar Download Job to the list of jobs to run.
* Before calling this function, you have to update the related conversation profileKey and avatarPointer fields with the urls which should be downloaded, or reset them if you wanted them reset.
*/ */
function shouldAddAvatarDownloadJob({ export function shouldAddAvatarDownloadJob({ conversationId }: { conversationId: string }) {
profileKeyHex, const conversation = getConversationController().get(conversationId);
profileUrl,
pubkey,
}: {
pubkey: string;
profileUrl: string | null | undefined;
profileKeyHex: string | null | undefined;
}) {
const conversation = getConversationController().get(pubkey);
if (!conversation) { if (!conversation) {
// return true so we do not retry this task. // return true so we do not retry this task.
window.log.warn('shouldAddAvatarDownloadJob did not corresponding conversation'); window.log.warn('shouldAddAvatarDownloadJob did not corresponding conversation');
@ -43,37 +36,24 @@ function shouldAddAvatarDownloadJob({
return false; return false;
} }
const prevPointer = conversation.get('avatarPointer'); const prevPointer = conversation.get('avatarPointer');
const profileKey = conversation.get('profileKey');
const hasNoAvatar = isEmpty(prevPointer) || isEmpty(profileKey);
if (!isEmpty(profileUrl) && !isEmpty(profileKeyHex) && !isEqual(prevPointer, profileUrl)) { if (hasNoAvatar) {
return true; return false;
} }
return false;
return true;
} }
async function addAvatarDownloadJobIfNeeded({ async function addAvatarDownloadJob({ conversationId }: { conversationId: string }) {
profileKeyHex, if (shouldAddAvatarDownloadJob({ conversationId })) {
profileUrl,
pubkey,
}: {
pubkey: string;
profileUrl: string | null | undefined;
profileKeyHex: string | null | undefined;
}) {
if (profileKeyHex && shouldAddAvatarDownloadJob({ pubkey, profileUrl, profileKeyHex })) {
const avatarDownloadJob = new AvatarDownloadJob({ const avatarDownloadJob = new AvatarDownloadJob({
conversationId: pubkey, conversationId,
profileKeyHex,
profilePictureUrl: profileUrl || null,
nextAttemptTimestamp: Date.now(), nextAttemptTimestamp: Date.now(),
}); });
window.log.debug( window.log.debug(`addAvatarDownloadJobIfNeeded: adding job download for ${conversationId} `);
`addAvatarDownloadJobIfNeeded: adding job download for ${pubkey}:${profileUrl}:${profileKeyHex} `
);
await runners.avatarDownloadRunner.addJob(avatarDownloadJob); await runners.avatarDownloadRunner.addJob(avatarDownloadJob);
} else {
// window.log.debug(
// `addAvatarDownloadJobIfNeeded: no download required for ${pubkey}:${profileUrl}:${profileKeyHex} `
// );
} }
} }
@ -89,12 +69,9 @@ class AvatarDownloadJob extends PersistedJob<AvatarDownloadPersistedData> {
nextAttemptTimestamp, nextAttemptTimestamp,
maxAttempts, maxAttempts,
currentRetry, currentRetry,
profileKeyHex,
profilePictureUrl,
identifier, identifier,
}: Pick<AvatarDownloadPersistedData, 'profileKeyHex' | 'profilePictureUrl'> & { }: Pick<AvatarDownloadPersistedData, 'conversationId'> &
conversationId: string; Partial<
} & Partial<
Pick< Pick<
AvatarDownloadPersistedData, AvatarDownloadPersistedData,
| 'nextAttemptTimestamp' | 'nextAttemptTimestamp'
@ -112,8 +89,6 @@ class AvatarDownloadJob extends PersistedJob<AvatarDownloadPersistedData> {
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttemps, maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttemps,
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries, nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
currentRetry: isNumber(currentRetry) ? currentRetry : 0, currentRetry: isNumber(currentRetry) ? currentRetry : 0,
profileKeyHex,
profilePictureUrl,
}); });
} }
@ -142,103 +117,81 @@ class AvatarDownloadJob extends PersistedJob<AvatarDownloadPersistedData> {
return RunJobResult.PermanentFailure; return RunJobResult.PermanentFailure;
} }
let changes = false; let changes = false;
const toDownloadPointer = conversation.get('avatarPointer');
const toDownloadProfileKey = conversation.get('profileKey');
// if there is an avatar and profileKey for that user ('', null and undefined excluded), download, decrypt and save the avatar locally.
if (toDownloadPointer && toDownloadProfileKey) {
try {
window.log.debug(`[profileupdate] starting downloading task for ${conversation.id}`);
const downloaded = await downloadAttachment({
url: toDownloadPointer,
isRaw: true,
});
conversation = getConversationController().getOrThrow(convoId);
const shouldRunJob = shouldAddAvatarDownloadJob({ if (!downloaded.data.byteLength) {
pubkey: convoId, window.log.debug(`[profileupdate] downloaded data is empty for ${conversation.id}`);
profileKeyHex: this.persistedData.profileKeyHex, return RunJobResult.RetryJobIfPossible; // so we retry this job
profileUrl: this.persistedData.profilePictureUrl, }
});
if (!shouldRunJob) {
// return true so we do not retry this task.
window.log.warn('AvatarDownloadJob shouldAddAvatarDownloadJob said no');
return RunJobResult.PermanentFailure;
}
if (this.persistedData.profilePictureUrl && this.persistedData.profileKeyHex) { // null => use placeholder with color and first letter
const prevPointer = conversation.get('avatarPointer'); let path = null;
const needsUpdate =
!prevPointer || !isEqual(prevPointer, this.persistedData.profilePictureUrl);
if (needsUpdate) {
try { try {
window.log.debug(`[profileupdate] starting downloading task for ${conversation.id}`); const profileKeyArrayBuffer = fromHexToArray(toDownloadProfileKey);
const downloaded = await downloadAttachment({ let decryptedData: ArrayBuffer;
url: this.persistedData.profilePictureUrl,
isRaw: true,
});
conversation = getConversationController().getOrThrow(convoId);
if (!downloaded.data.byteLength) {
window.log.debug(`[profileupdate] downloaded data is empty for ${conversation.id}`);
return RunJobResult.RetryJobIfPossible; // so we retry this job
}
// null => use placeholder with color and first letter
let path = null;
try { try {
const profileKeyArrayBuffer = fromHexToArray(this.persistedData.profileKeyHex); decryptedData = await decryptProfile(downloaded.data, profileKeyArrayBuffer);
let decryptedData: ArrayBuffer; } catch (decryptError) {
try {
decryptedData = await decryptProfile(downloaded.data, profileKeyArrayBuffer);
} catch (decryptError) {
window.log.info(
`[profileupdate] failed to decrypt downloaded data ${conversation.id} with provided profileKey`
);
// if we cannot decrypt the content, there is no need to keep retrying.
return RunJobResult.PermanentFailure;
}
window.log.info( window.log.info(
`[profileupdate] about to auto scale avatar for convo ${conversation.id}` `[profileupdate] failed to decrypt downloaded data ${conversation.id} with provided profileKey`
); );
// if we got content, but cannot decrypt it with the provided profileKey, there is no need to keep retrying.
return RunJobResult.PermanentFailure;
}
const scaledData = await autoScaleForIncomingAvatar(decryptedData); window.log.info(
`[profileupdate] about to auto scale avatar for convo ${conversation.id}`
);
const upgraded = await processNewAttachment({ // we autoscale incoming avatars because our app keeps decrypted avatars in memory and some platforms allows large avatars to be uploaded.
data: await scaledData.blob.arrayBuffer(), const scaledData = await autoScaleForIncomingAvatar(decryptedData);
contentType: MIME.IMAGE_UNKNOWN, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
});
conversation = getConversationController().getOrThrow(convoId);
// Only update the convo if the download and decrypt is a success const upgraded = await processNewAttachment({
conversation.set('avatarPointer', this.persistedData.profilePictureUrl); data: await scaledData.blob.arrayBuffer(),
conversation.set('profileKey', this.persistedData.profileKeyHex || undefined); contentType: MIME.IMAGE_UNKNOWN, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
({ path } = upgraded); });
} catch (e) { conversation = getConversationController().getOrThrow(convoId);
window?.log?.error(`[profileupdate] Could not decrypt profile image: ${e}`); ({ path } = upgraded);
return RunJobResult.RetryJobIfPossible; // so we retry this job } catch (e) {
} window?.log?.error(`[profileupdate] Could not decrypt profile image: ${e}`);
return RunJobResult.RetryJobIfPossible; // so we retry this job
}
conversation.set({ avatarInProfile: path || undefined }); conversation.set({ avatarInProfile: path || undefined });
changes = true; changes = true;
} catch (e) { } catch (e) {
if (isString(e.message) && (e.message as string).includes('404')) { // TODO would be nice to throw a specific exception here instead of relying on the error string.
window.log.warn( if (isString(e.message) && (e.message as string).includes('404')) {
`[profileupdate] Failed to download attachment at ${this.persistedData.profilePictureUrl}. We got 404 error: "${e.message}"`
);
return RunJobResult.PermanentFailure;
}
window.log.warn( window.log.warn(
`[profileupdate] Failed to download attachment at ${this.persistedData.profilePictureUrl}. Maybe it expired? ${e.message}` `[profileupdate] Failed to download attachment at ${toDownloadPointer}. We got 404 error: "${e.message}"`
); );
return RunJobResult.RetryJobIfPossible; return RunJobResult.PermanentFailure;
} }
window.log.warn(
`[profileupdate] Failed to download attachment at ${toDownloadPointer}. Maybe it expired? ${e.message}`
);
return RunJobResult.RetryJobIfPossible;
} }
} else { } else {
if ( // there is no valid avatar to download, make sure the local file of the avatar of that user is removed
conversation.get('avatarInProfile') || if (conversation.get('avatarInProfile')) {
conversation.get('avatarPointer') ||
conversation.get('profileKey')
) {
changes = true;
conversation.set({ conversation.set({
avatarInProfile: undefined, avatarInProfile: undefined,
avatarPointer: undefined,
profileKey: undefined,
}); });
changes = true;
} }
} }
@ -270,27 +223,20 @@ class AvatarDownloadJob extends PersistedJob<AvatarDownloadPersistedData> {
return super.serializeBase(); return super.serializeBase();
} }
public nonRunningJobsToRemove(jobs: Array<AvatarDownloadPersistedData>) { public nonRunningJobsToRemove(_jobs: Array<AvatarDownloadPersistedData>) {
// for an avatar download job, we want to remove any job matching the same conversationID. return [];
return jobs.filter(j => j.conversationId === this.persistedData.conversationId);
} }
public addJobCheck(jobs: Array<AvatarDownloadPersistedData>): AddJobCheckReturn { public addJobCheck(jobs: Array<AvatarDownloadPersistedData>): AddJobCheckReturn {
// avoid adding the same job if the exact same one is already planned // avoid adding the same job if the exact same one is already planned
const hasSameJob = jobs.some(j => { const hasSameJob = jobs.some(j => {
return ( return j.conversationId === this.persistedData.conversationId;
j.conversationId === this.persistedData.conversationId &&
j.profileKeyHex === this.persistedData.profileKeyHex &&
j.profilePictureUrl === this.persistedData.profilePictureUrl
);
}); });
if (hasSameJob) { if (hasSameJob) {
return 'skipAddSameJobPresent'; return 'skipAddSameJobPresent';
} }
if (this.nonRunningJobsToRemove(jobs).length) {
return 'removeJobsFromQueue';
}
return null; return null;
} }
@ -301,5 +247,5 @@ class AvatarDownloadJob extends PersistedJob<AvatarDownloadPersistedData> {
export const AvatarDownload = { export const AvatarDownload = {
AvatarDownloadJob, AvatarDownloadJob,
addAvatarDownloadJobIfNeeded, addAvatarDownloadJob,
}; };

Loading…
Cancel
Save