mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
252 lines
8.8 KiB
TypeScript
252 lines
8.8 KiB
TypeScript
import { isNumber } from 'lodash';
|
|
import { v4 } from 'uuid';
|
|
import { processNewAttachment } from '../../../../types/MessageAttachment';
|
|
import { ConvoHub } from '../../../conversations';
|
|
import { runners } from '../JobRunner';
|
|
import {
|
|
AddJobCheckReturn,
|
|
PersistedJob,
|
|
RunJobResult,
|
|
type AvatarMigratePersistedData,
|
|
} from '../PersistedJob';
|
|
import { processAvatarData } from '../../../../util/avatar/processAvatarData';
|
|
import { DecryptedAttachmentsManager } from '../../../crypto/DecryptedAttachmentsManager';
|
|
import { IMAGE_JPEG } from '../../../../types/MIME';
|
|
import { NetworkTime } from '../../../../util/NetworkTime';
|
|
import {
|
|
SessionProfileResetAvatarPrivate,
|
|
SessionProfileSetAvatarDownloadedAny,
|
|
} from '../../../../models/profile';
|
|
|
|
const defaultMsBetweenRetries = 10000;
|
|
const defaultMaxAttempts = 3;
|
|
|
|
function shouldAddAvatarMigrateJob({ conversationId }: { conversationId: string }) {
|
|
const conversation = ConvoHub.use().get(conversationId);
|
|
if (!conversation) {
|
|
window.log.warn('shouldAddAvatarMigrateJob: no corresponding conversation');
|
|
|
|
return false;
|
|
}
|
|
if (!conversation.isPrivate()) {
|
|
window.log.debug('shouldAddAvatarMigrateJob can only be used for private chats');
|
|
return false;
|
|
}
|
|
const hasAvatar = conversation.getAvatarInProfilePath();
|
|
const hasFallbackSet = conversation.getFallbackAvatarInProfilePath();
|
|
|
|
if (!hasAvatar) {
|
|
return false;
|
|
}
|
|
|
|
if (hasFallbackSet) {
|
|
// this means we have processed that conversation already. No need to migrate it.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function addAvatarMigrateJob({ conversationId }: { conversationId: string }) {
|
|
if (shouldAddAvatarMigrateJob({ conversationId })) {
|
|
const avatarMigrateJob = new AvatarMigrateJob({
|
|
conversationId,
|
|
nextAttemptTimestamp: Date.now(),
|
|
});
|
|
window.log.debug(`addAvatarMigrateJob: adding job migrate for ${conversationId} `);
|
|
await runners.avatarMigrateRunner.addJob(avatarMigrateJob);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a temporary job runner to migrate the avatar of a conversation to the new avatar system.
|
|
* We should probably remove it in 6 months time.
|
|
* What is does, is generated the static images from the avatars that are animated, and store them in the database.
|
|
*/
|
|
class AvatarMigrateJob extends PersistedJob<AvatarMigratePersistedData> {
|
|
constructor({
|
|
conversationId,
|
|
nextAttemptTimestamp,
|
|
maxAttempts,
|
|
currentRetry,
|
|
identifier,
|
|
}: Pick<AvatarMigratePersistedData, 'conversationId'> &
|
|
Partial<
|
|
Pick<
|
|
AvatarMigratePersistedData,
|
|
'nextAttemptTimestamp' | 'identifier' | 'maxAttempts' | 'currentRetry'
|
|
>
|
|
>) {
|
|
super({
|
|
jobType: 'AvatarMigrateJobType',
|
|
identifier: identifier || v4(),
|
|
conversationId,
|
|
delayBetweenRetries: defaultMsBetweenRetries,
|
|
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts,
|
|
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
|
|
currentRetry: isNumber(currentRetry) ? currentRetry : 0,
|
|
});
|
|
}
|
|
|
|
public async run(): Promise<RunJobResult> {
|
|
const convoId = this.persistedData.conversationId;
|
|
|
|
window.log.debug(`running job ${this.persistedData.jobType} with conversationId:"${convoId}"`);
|
|
|
|
if (!this.persistedData.identifier || !convoId) {
|
|
return RunJobResult.Success;
|
|
}
|
|
|
|
let conversation = ConvoHub.use().get(convoId);
|
|
if (!conversation) {
|
|
window.log.warn('AvatarMigrateJob did not corresponding conversation');
|
|
|
|
return RunJobResult.Success;
|
|
}
|
|
if (!conversation.isPrivate()) {
|
|
window.log.warn('AvatarMigrateJob can only be used for private or groupv2 convos');
|
|
return RunJobResult.Success;
|
|
}
|
|
|
|
const existingAvatarPointer = conversation.getAvatarPointer();
|
|
if (!existingAvatarPointer) {
|
|
window.log.warn('AvatarMigrateJob: no avatar pointer found for conversation');
|
|
return RunJobResult.Success;
|
|
}
|
|
const existingProfileKeyHex = conversation.getProfileKeyHex();
|
|
if (!existingProfileKeyHex) {
|
|
window.log.warn('AvatarMigrateJob: no profileKey found for conversation');
|
|
return RunJobResult.Success;
|
|
}
|
|
const avatarPath = conversation.getAvatarInProfilePath();
|
|
if (!avatarPath) {
|
|
window.log.warn('AvatarMigrateJob: no avatar path found for conversation');
|
|
return RunJobResult.Success;
|
|
}
|
|
|
|
try {
|
|
const decryptedBlob = await DecryptedAttachmentsManager.getDecryptedBlob(
|
|
avatarPath,
|
|
IMAGE_JPEG
|
|
);
|
|
|
|
const decryptedData = await decryptedBlob.arrayBuffer();
|
|
|
|
if (!decryptedData) {
|
|
const profile = new SessionProfileResetAvatarPrivate({
|
|
convo: conversation,
|
|
displayName: null, // null to not update the display name.
|
|
// this is the AvatarMigrateJob.
|
|
// We want to override the avatars that was stored for that user
|
|
// as we can't decrypt it.
|
|
profileUpdatedAtSeconds: NetworkTime.nowSeconds(),
|
|
// Don't overwrite those if they are set
|
|
proDetails: { bitsetProFeatures: null, proExpiryTsMs: null, proGenIndexHashB64: null },
|
|
});
|
|
await profile.applyChangesIfNeeded();
|
|
|
|
return RunJobResult.Success;
|
|
}
|
|
|
|
// we autoscale incoming avatars because our app keeps decrypted avatars in memory and some platforms allows large avatars to be uploaded.
|
|
const processed = await processAvatarData(decryptedData, conversation.isMe(), true);
|
|
|
|
const upgradedMainAvatar = await processNewAttachment({
|
|
data: processed.mainAvatarDetails.outputBuffer,
|
|
contentType: processed.mainAvatarDetails.contentType,
|
|
});
|
|
const upgradedFallbackAvatar = processed.avatarFallback
|
|
? await processNewAttachment({
|
|
data: processed.avatarFallback.outputBuffer,
|
|
contentType: processed.avatarFallback.contentType,
|
|
})
|
|
: null;
|
|
const mainAvatarPath = upgradedMainAvatar.path;
|
|
const fallbackAvatarPath = upgradedFallbackAvatar?.path || upgradedMainAvatar.path;
|
|
conversation = ConvoHub.use().getOrThrow(convoId);
|
|
|
|
const profile = new SessionProfileSetAvatarDownloadedAny({
|
|
convo: conversation,
|
|
displayName: null, // null to not update the display name.
|
|
avatarPath: mainAvatarPath,
|
|
fallbackAvatarPath,
|
|
avatarPointer: existingAvatarPointer,
|
|
profileKey: existingProfileKeyHex,
|
|
});
|
|
|
|
await profile.applyChangesIfNeeded();
|
|
|
|
return RunJobResult.Success;
|
|
} catch (e) {
|
|
window.log.warn(`failed to process avatar for ${convoId}`, e.message);
|
|
// if we failed multiple times, and before we are out of retries, remove the saved avatar altogether.
|
|
// it will be re downloaded by the AvatarDownloadJob if needed
|
|
conversation = ConvoHub.use().get(convoId);
|
|
|
|
if (
|
|
conversation &&
|
|
(conversation.getAvatarInProfilePath() || conversation.getFallbackAvatarInProfilePath())
|
|
) {
|
|
// there is no valid avatar to download, make sure the local file of the avatar of that user is removed
|
|
const profile = new SessionProfileResetAvatarPrivate({
|
|
convo: conversation,
|
|
displayName: null, // null to not update the display name.
|
|
// this is the AvatarMigrateJob.
|
|
// We want to override the avatar that was stored for that user
|
|
// as we can't decrypt it.
|
|
profileUpdatedAtSeconds: NetworkTime.nowSeconds(),
|
|
// Don't overwrite those if they are set
|
|
proDetails: { bitsetProFeatures: null, proExpiryTsMs: null, proGenIndexHashB64: null },
|
|
});
|
|
await profile.applyChangesIfNeeded();
|
|
}
|
|
return RunJobResult.RetryJobIfPossible;
|
|
}
|
|
}
|
|
|
|
public serializeJob(): AvatarMigratePersistedData {
|
|
return super.serializeBase();
|
|
}
|
|
|
|
public nonRunningJobsToRemove(_jobs: Array<AvatarMigratePersistedData>) {
|
|
return [];
|
|
}
|
|
|
|
public addJobCheck(jobs: Array<AvatarMigratePersistedData>): AddJobCheckReturn {
|
|
// avoid adding the same job if the exact same one is already planned
|
|
const hasSameJob = jobs.some(j => {
|
|
return j.conversationId === this.persistedData.conversationId;
|
|
});
|
|
|
|
if (hasSameJob) {
|
|
return 'skipAddSameJobPresent';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public getJobTimeoutMs(): number {
|
|
return 10000;
|
|
}
|
|
}
|
|
|
|
async function scheduleAllAvatarMigrateJobs() {
|
|
const conversations = ConvoHub.use().getConversations();
|
|
|
|
for (let index = 0; index < conversations.length; index++) {
|
|
const conversation = conversations[index];
|
|
if (shouldAddAvatarMigrateJob({ conversationId: conversation.id })) {
|
|
window.log.debug(`[avatarMigrate] scheduling avatar migrate job for ${conversation.id}`);
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await addAvatarMigrateJob({
|
|
conversationId: conversation.id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const AvatarMigrate = {
|
|
scheduleAllAvatarMigrateJobs,
|
|
AvatarMigrateJob,
|
|
};
|