2023-01-03 19:55:46 +00:00
|
|
|
// Copyright 2019 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-07-28 21:37:09 +00:00
|
|
|
import { isNumber, omit } from 'lodash';
|
2021-06-17 17:15:10 +00:00
|
|
|
import { v4 as getGuid } from 'uuid';
|
|
|
|
|
|
|
|
import dataInterface from '../sql/Client';
|
2021-08-26 14:10:58 +00:00
|
|
|
import * as durations from '../util/durations';
|
2022-02-25 18:37:15 +00:00
|
|
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
2022-05-23 23:07:41 +00:00
|
|
|
import { strictAssert } from '../util/assert';
|
2021-06-17 17:15:10 +00:00
|
|
|
import { downloadAttachment } from '../util/downloadAttachment';
|
2021-09-24 00:49:05 +00:00
|
|
|
import * as Bytes from '../Bytes';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type {
|
2021-06-17 17:15:10 +00:00
|
|
|
AttachmentDownloadJobType,
|
|
|
|
AttachmentDownloadJobTypeType,
|
|
|
|
} from '../sql/Interface';
|
|
|
|
|
2023-10-30 16:24:28 +00:00
|
|
|
import { getValue } from '../RemoteConfig';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { MessageModel } from '../models/messages';
|
|
|
|
import type { AttachmentType } from '../types/Attachment';
|
2023-10-30 16:24:28 +00:00
|
|
|
import {
|
|
|
|
AttachmentSizeError,
|
|
|
|
getAttachmentSignature,
|
|
|
|
isDownloaded,
|
|
|
|
} from '../types/Attachment';
|
2022-05-23 23:07:41 +00:00
|
|
|
import * as Errors from '../types/errors';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { LoggerType } from '../types/Logging';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../logging/log';
|
2023-10-30 16:24:28 +00:00
|
|
|
import {
|
|
|
|
KIBIBYTE,
|
|
|
|
getMaximumIncomingAttachmentSizeInKb,
|
2023-12-18 18:14:59 +00:00
|
|
|
getMaximumIncomingTextAttachmentSizeInKb,
|
2023-10-30 16:24:28 +00:00
|
|
|
} from '../types/AttachmentSize';
|
2024-03-21 20:02:12 +00:00
|
|
|
import { redactCdnKey } from '../util/privacy';
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
const {
|
|
|
|
getMessageById,
|
2022-05-23 23:07:41 +00:00
|
|
|
getAttachmentDownloadJobById,
|
2019-01-30 20:15:07 +00:00
|
|
|
getNextAttachmentDownloadJobs,
|
|
|
|
removeAttachmentDownloadJob,
|
|
|
|
resetAttachmentDownloadPending,
|
|
|
|
saveAttachmentDownloadJob,
|
|
|
|
saveMessage,
|
|
|
|
setAttachmentDownloadJobPending,
|
2021-06-17 17:15:10 +00:00
|
|
|
} = dataInterface;
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
|
|
|
|
|
2021-08-26 14:10:58 +00:00
|
|
|
const TICK_INTERVAL = durations.MINUTE;
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
const RETRY_BACKOFF: Record<number, number> = {
|
2021-08-26 14:10:58 +00:00
|
|
|
1: 30 * durations.SECOND,
|
|
|
|
2: 30 * durations.MINUTE,
|
|
|
|
3: 6 * durations.HOUR,
|
2019-01-30 20:15:07 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let enabled = false;
|
2021-06-17 17:15:10 +00:00
|
|
|
let timeout: NodeJS.Timeout | null;
|
|
|
|
let logger: LoggerType;
|
2021-11-11 22:43:05 +00:00
|
|
|
const _activeAttachmentDownloadJobs: Record<string, Promise<void> | undefined> =
|
|
|
|
{};
|
2021-06-17 17:15:10 +00:00
|
|
|
|
|
|
|
type StartOptionsType = {
|
|
|
|
logger: LoggerType;
|
|
|
|
};
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
export async function start(options: StartOptionsType): Promise<void> {
|
2021-07-28 21:37:09 +00:00
|
|
|
({ logger } = options);
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!logger) {
|
|
|
|
throw new Error('attachment_downloads/start: logger must be provided!');
|
|
|
|
}
|
|
|
|
|
2020-06-09 22:33:37 +00:00
|
|
|
logger.info('attachment_downloads/start: enabling');
|
2019-01-30 20:15:07 +00:00
|
|
|
enabled = true;
|
|
|
|
await resetAttachmentDownloadPending();
|
|
|
|
|
2022-12-21 18:41:48 +00:00
|
|
|
void _tick();
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
export async function stop(): Promise<void> {
|
2021-05-06 21:32:23 +00:00
|
|
|
// If `.start()` wasn't called - the `logger` is `undefined`
|
|
|
|
if (logger) {
|
|
|
|
logger.info('attachment_downloads/stop: disabling');
|
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
enabled = false;
|
2022-02-25 18:37:15 +00:00
|
|
|
clearTimeoutIfNecessary(timeout);
|
|
|
|
timeout = null;
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
export async function addJob(
|
|
|
|
attachment: AttachmentType,
|
2023-04-20 16:31:59 +00:00
|
|
|
// TODO: DESKTOP-5279
|
2021-06-17 17:15:10 +00:00
|
|
|
job: { messageId: string; type: AttachmentDownloadJobTypeType; index: number }
|
|
|
|
): Promise<AttachmentType> {
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!attachment) {
|
|
|
|
throw new Error('attachments_download/addJob: attachment is required');
|
|
|
|
}
|
|
|
|
|
|
|
|
const { messageId, type, index } = job;
|
|
|
|
if (!messageId) {
|
|
|
|
throw new Error('attachments_download/addJob: job.messageId is required');
|
|
|
|
}
|
|
|
|
if (!type) {
|
|
|
|
throw new Error('attachments_download/addJob: job.type is required');
|
|
|
|
}
|
|
|
|
if (!isNumber(index)) {
|
|
|
|
throw new Error('attachments_download/addJob: index must be a number');
|
|
|
|
}
|
|
|
|
|
2022-05-23 23:07:41 +00:00
|
|
|
if (attachment.downloadJobId) {
|
|
|
|
let existingJob = await getAttachmentDownloadJobById(
|
|
|
|
attachment.downloadJobId
|
|
|
|
);
|
|
|
|
if (existingJob) {
|
|
|
|
// Reset job attempts through user's explicit action
|
|
|
|
existingJob = { ...existingJob, attempts: 0 };
|
|
|
|
|
|
|
|
if (_activeAttachmentDownloadJobs[existingJob.id]) {
|
|
|
|
logger.info(
|
|
|
|
`attachment_downloads/addJob: ${existingJob.id} already running`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
logger.info(
|
|
|
|
`attachment_downloads/addJob: restarting existing job ${existingJob.id}`
|
|
|
|
);
|
|
|
|
_activeAttachmentDownloadJobs[existingJob.id] = _runJob(existingJob);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...attachment,
|
|
|
|
pending: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
const id = getGuid();
|
|
|
|
const timestamp = Date.now();
|
2021-06-17 17:15:10 +00:00
|
|
|
const toSave: AttachmentDownloadJobType = {
|
2019-01-30 20:15:07 +00:00
|
|
|
...job,
|
|
|
|
id,
|
|
|
|
attachment,
|
|
|
|
timestamp,
|
|
|
|
pending: 0,
|
|
|
|
attempts: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
await saveAttachmentDownloadJob(toSave);
|
|
|
|
|
2022-12-21 18:41:48 +00:00
|
|
|
void _maybeStartJob();
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...attachment,
|
|
|
|
pending: true,
|
|
|
|
downloadJobId: id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
async function _tick(): Promise<void> {
|
2022-02-25 18:37:15 +00:00
|
|
|
clearTimeoutIfNecessary(timeout);
|
|
|
|
timeout = null;
|
2019-06-24 20:39:37 +00:00
|
|
|
|
2022-12-21 18:41:48 +00:00
|
|
|
void _maybeStartJob();
|
2019-01-30 20:15:07 +00:00
|
|
|
timeout = setTimeout(_tick, TICK_INTERVAL);
|
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
async function _maybeStartJob(): Promise<void> {
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!enabled) {
|
2020-06-09 22:33:37 +00:00
|
|
|
logger.info('attachment_downloads/_maybeStartJob: not enabled, returning');
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const jobCount = getActiveJobCount();
|
|
|
|
const limit = MAX_ATTACHMENT_JOB_PARALLELISM - jobCount;
|
|
|
|
if (limit <= 0) {
|
2021-06-01 17:13:10 +00:00
|
|
|
logger.info(
|
|
|
|
'attachment_downloads/_maybeStartJob: reached active job limit, waiting'
|
|
|
|
);
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const nextJobs = await getNextAttachmentDownloadJobs(limit);
|
|
|
|
if (nextJobs.length <= 0) {
|
2021-06-01 17:13:10 +00:00
|
|
|
logger.info(
|
|
|
|
'attachment_downloads/_maybeStartJob: no attachment jobs to run'
|
|
|
|
);
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// To prevent the race condition caused by two parallel database calls, eached kicked
|
|
|
|
// off because the jobCount wasn't at the max.
|
|
|
|
const secondJobCount = getActiveJobCount();
|
|
|
|
const needed = MAX_ATTACHMENT_JOB_PARALLELISM - secondJobCount;
|
|
|
|
if (needed <= 0) {
|
2021-06-01 17:13:10 +00:00
|
|
|
logger.info(
|
|
|
|
'attachment_downloads/_maybeStartJob: reached active job limit after ' +
|
|
|
|
'db query, waiting'
|
|
|
|
);
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const jobs = nextJobs.slice(0, Math.min(needed, nextJobs.length));
|
2021-06-01 17:13:10 +00:00
|
|
|
|
|
|
|
logger.info(
|
|
|
|
`attachment_downloads/_maybeStartJob: starting ${jobs.length} jobs`
|
|
|
|
);
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
for (let i = 0, max = jobs.length; i < max; i += 1) {
|
|
|
|
const job = jobs[i];
|
2019-09-17 21:09:01 +00:00
|
|
|
const existing = _activeAttachmentDownloadJobs[job.id];
|
|
|
|
if (existing) {
|
2022-08-02 02:25:53 +00:00
|
|
|
logger.warn(
|
|
|
|
`attachment_downloads/_maybeStartJob: Job ${job.id} is already running`
|
|
|
|
);
|
2019-09-17 21:09:01 +00:00
|
|
|
} else {
|
2022-08-02 02:25:53 +00:00
|
|
|
logger.info(
|
|
|
|
`attachment_downloads/_maybeStartJob: Starting job ${job.id}`
|
|
|
|
);
|
|
|
|
const promise = _runJob(job);
|
|
|
|
_activeAttachmentDownloadJobs[job.id] = promise;
|
|
|
|
|
|
|
|
const postProcess = async () => {
|
|
|
|
const logId = `attachment_downloads/_maybeStartJob/postProcess/${job.id}`;
|
|
|
|
try {
|
|
|
|
await promise;
|
|
|
|
if (_activeAttachmentDownloadJobs[job.id]) {
|
|
|
|
throw new Error(
|
|
|
|
`${logId}: Active attachments jobs list still has this job!`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
log.error(
|
|
|
|
`${logId}: Download job threw an error, deleting.`,
|
|
|
|
Errors.toLogFormat(error)
|
|
|
|
);
|
|
|
|
|
|
|
|
delete _activeAttachmentDownloadJobs[job.id];
|
|
|
|
try {
|
2022-08-04 00:38:41 +00:00
|
|
|
await _markAttachmentAsFailed(job);
|
2022-08-02 02:25:53 +00:00
|
|
|
} catch (deleteError) {
|
|
|
|
log.error(
|
|
|
|
`${logId}: Failed to delete attachment job`,
|
2023-01-13 19:18:59 +00:00
|
|
|
Errors.toLogFormat(deleteError)
|
2022-08-02 02:25:53 +00:00
|
|
|
);
|
|
|
|
} finally {
|
2022-12-21 18:41:48 +00:00
|
|
|
void _maybeStartJob();
|
2022-08-02 02:25:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Note: intentionally not awaiting
|
2022-12-21 18:41:48 +00:00
|
|
|
void postProcess();
|
2019-09-17 21:09:01 +00:00
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
|
|
|
|
if (!job) {
|
2022-05-23 23:07:41 +00:00
|
|
|
log.warn('attachment_downloads/_runJob: Job was missing!');
|
2021-06-17 17:15:10 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { id, messageId, attachment, type, index, attempts } = job;
|
2019-01-30 20:15:07 +00:00
|
|
|
let message;
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (!job || !attachment || !messageId) {
|
|
|
|
throw new Error(
|
|
|
|
`_runJob: Key information required for job was missing. Job id: ${id}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const pending = true;
|
|
|
|
await setAttachmentDownloadJobPending(id, pending);
|
|
|
|
|
2022-08-04 00:38:41 +00:00
|
|
|
message = await _getMessageById(id, messageId);
|
2024-03-21 20:02:12 +00:00
|
|
|
logger.info(
|
|
|
|
'attachment_downloads/_runJob' +
|
|
|
|
`(jobId: ${id}, type: ${type}, index: ${index},` +
|
|
|
|
` cdnKey: ${
|
|
|
|
attachment.cdnKey ? redactCdnKey(attachment.cdnKey) : null
|
|
|
|
},` +
|
|
|
|
` messageTimestamp: ${message?.attributes.timestamp}): starting`
|
|
|
|
);
|
2022-05-23 23:07:41 +00:00
|
|
|
|
2022-08-04 00:38:41 +00:00
|
|
|
if (!message) {
|
|
|
|
return;
|
2022-05-23 23:07:41 +00:00
|
|
|
}
|
|
|
|
|
2023-10-30 16:24:28 +00:00
|
|
|
let downloaded: AttachmentType | null = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const maxInKib = getMaximumIncomingAttachmentSizeInKb(getValue);
|
2023-12-18 18:14:59 +00:00
|
|
|
const maxTextAttachmentSizeInKib =
|
|
|
|
getMaximumIncomingTextAttachmentSizeInKb(getValue);
|
|
|
|
|
|
|
|
const { size } = attachment;
|
2023-10-30 16:24:28 +00:00
|
|
|
const sizeInKib = size / KIBIBYTE;
|
2023-12-18 18:14:59 +00:00
|
|
|
|
2024-03-01 19:15:24 +00:00
|
|
|
if (!Number.isFinite(size) || size < 0 || sizeInKib > maxInKib) {
|
2023-10-30 16:24:28 +00:00
|
|
|
throw new AttachmentSizeError(
|
|
|
|
`Attachment Job ${id}: Attachment was ${sizeInKib}kib, max is ${maxInKib}kib`
|
|
|
|
);
|
|
|
|
}
|
2023-12-18 18:14:59 +00:00
|
|
|
if (type === 'long-message' && sizeInKib > maxTextAttachmentSizeInKib) {
|
|
|
|
throw new AttachmentSizeError(
|
|
|
|
`Attachment Job ${id}: Text attachment was ${sizeInKib}kib, max is ${maxTextAttachmentSizeInKib}kib`
|
|
|
|
);
|
|
|
|
}
|
2022-05-23 23:07:41 +00:00
|
|
|
|
2023-10-30 16:24:28 +00:00
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
|
|
|
{ ...attachment, pending: true },
|
|
|
|
{ type, index }
|
|
|
|
);
|
|
|
|
|
|
|
|
// If the download is bigger than expected, we'll stop in the middle
|
|
|
|
downloaded = await downloadAttachment(attachment);
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof AttachmentSizeError) {
|
|
|
|
log.error(Errors.toLogFormat(error));
|
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
|
|
|
_markAttachmentAsTooBig(attachment),
|
|
|
|
{ type, index }
|
|
|
|
);
|
|
|
|
await _finishJob(message, id);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
2020-07-07 00:39:55 +00:00
|
|
|
|
|
|
|
if (!downloaded) {
|
|
|
|
logger.warn(
|
2022-05-23 23:07:41 +00:00
|
|
|
`attachment_downloads/_runJob(${id}): Got 404 from server for CDN ${
|
2020-07-07 00:39:55 +00:00
|
|
|
attachment.cdnNumber
|
2020-11-18 15:15:42 +00:00
|
|
|
}, marking attachment ${
|
|
|
|
attachment.cdnId || attachment.cdnKey
|
|
|
|
} from message ${message.idForLogging()} as permanent error`
|
2020-07-07 00:39:55 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
2022-05-23 23:07:41 +00:00
|
|
|
_markAttachmentAsPermanentError(attachment),
|
2020-07-07 00:39:55 +00:00
|
|
|
{ type, index }
|
|
|
|
);
|
2022-05-23 23:07:41 +00:00
|
|
|
await _finishJob(message, id);
|
2020-07-07 00:39:55 +00:00
|
|
|
return;
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
2024-03-20 15:23:31 +00:00
|
|
|
logger.info(
|
|
|
|
`attachment_downloads/_runJob(${id}): processing new attachment` +
|
|
|
|
` of type: ${type}`
|
|
|
|
);
|
2021-11-11 22:43:05 +00:00
|
|
|
const upgradedAttachment =
|
|
|
|
await window.Signal.Migrations.processNewAttachment(downloaded);
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2022-05-23 23:07:41 +00:00
|
|
|
await _addAttachmentToMessage(message, omit(upgradedAttachment, 'error'), {
|
|
|
|
type,
|
|
|
|
index,
|
|
|
|
});
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
await _finishJob(message, id);
|
|
|
|
} catch (error) {
|
2021-06-17 17:15:10 +00:00
|
|
|
const logId = message ? message.idForLogging() : id || '<no id>';
|
2019-01-30 20:15:07 +00:00
|
|
|
const currentAttempt = (attempts || 0) + 1;
|
|
|
|
|
|
|
|
if (currentAttempt >= 3) {
|
|
|
|
logger.error(
|
2022-05-23 23:07:41 +00:00
|
|
|
`attachment_downloads/runJob(${id}): ${currentAttempt} failed ` +
|
|
|
|
`attempts, marking attachment from message ${logId} as ` +
|
|
|
|
'error:',
|
|
|
|
Errors.toLogFormat(error)
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
|
2022-06-06 22:13:21 +00:00
|
|
|
try {
|
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
|
|
|
_markAttachmentAsTransientError(attachment),
|
|
|
|
{ type, index }
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
await _finishJob(message, id);
|
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.error(
|
2022-05-23 23:07:41 +00:00
|
|
|
`attachment_downloads/_runJob(${id}): Failed to download attachment ` +
|
|
|
|
`type ${type} for message ${logId}, attempt ${currentAttempt}:`,
|
|
|
|
Errors.toLogFormat(error)
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
|
2022-08-16 23:49:47 +00:00
|
|
|
try {
|
|
|
|
// Remove `pending` flag from the attachment.
|
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
|
|
|
{
|
|
|
|
...attachment,
|
|
|
|
downloadJobId: id,
|
|
|
|
},
|
|
|
|
{ type, index }
|
|
|
|
);
|
|
|
|
if (message) {
|
|
|
|
await saveMessage(message.attributes, {
|
2023-08-10 16:43:33 +00:00
|
|
|
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
2022-08-16 23:49:47 +00:00
|
|
|
});
|
|
|
|
}
|
2022-05-23 23:07:41 +00:00
|
|
|
|
2022-08-16 23:49:47 +00:00
|
|
|
const failedJob = {
|
|
|
|
...job,
|
|
|
|
pending: 0,
|
|
|
|
attempts: currentAttempt,
|
|
|
|
timestamp:
|
|
|
|
Date.now() + (RETRY_BACKOFF[currentAttempt] || RETRY_BACKOFF[3]),
|
|
|
|
};
|
2022-06-06 22:13:21 +00:00
|
|
|
|
2022-08-16 23:49:47 +00:00
|
|
|
await saveAttachmentDownloadJob(failedJob);
|
|
|
|
} finally {
|
|
|
|
delete _activeAttachmentDownloadJobs[id];
|
2022-12-21 18:41:48 +00:00
|
|
|
void _maybeStartJob();
|
2022-08-16 23:49:47 +00:00
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-04 00:38:41 +00:00
|
|
|
async function _markAttachmentAsFailed(
|
|
|
|
job: AttachmentDownloadJobType
|
|
|
|
): Promise<void> {
|
|
|
|
const { id, messageId, attachment, type, index } = job;
|
|
|
|
const message = await _getMessageById(id, messageId);
|
|
|
|
|
2023-01-13 19:18:59 +00:00
|
|
|
try {
|
|
|
|
if (!message) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await _addAttachmentToMessage(
|
|
|
|
message,
|
|
|
|
_markAttachmentAsPermanentError(attachment),
|
|
|
|
{ type, index }
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
await _finishJob(message, id);
|
2022-08-04 00:38:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _getMessageById(
|
|
|
|
id: string,
|
|
|
|
messageId: string
|
|
|
|
): Promise<MessageModel | undefined> {
|
2023-10-04 00:12:57 +00:00
|
|
|
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
2022-08-04 00:38:41 +00:00
|
|
|
|
|
|
|
if (message) {
|
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
|
|
|
const messageAttributes = await getMessageById(messageId);
|
|
|
|
if (!messageAttributes) {
|
|
|
|
logger.error(
|
|
|
|
`attachment_downloads/_runJob(${id}): ` +
|
|
|
|
'Source message not found, deleting job'
|
|
|
|
);
|
|
|
|
await _finishJob(null, id);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
strictAssert(messageId === messageAttributes.id, 'message id mismatch');
|
2023-10-04 00:12:57 +00:00
|
|
|
return window.MessageCache.__DEPRECATED$register(
|
|
|
|
messageId,
|
|
|
|
messageAttributes,
|
|
|
|
'AttachmentDownloads._getMessageById'
|
|
|
|
);
|
2022-08-04 00:38:41 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
async function _finishJob(
|
|
|
|
message: MessageModel | null | undefined,
|
|
|
|
id: string
|
|
|
|
): Promise<void> {
|
2019-01-30 20:15:07 +00:00
|
|
|
if (message) {
|
2020-06-09 22:33:37 +00:00
|
|
|
logger.info(`attachment_downloads/_finishJob for job id: ${id}`);
|
2021-12-20 21:04:02 +00:00
|
|
|
await saveMessage(message.attributes, {
|
2023-08-10 16:43:33 +00:00
|
|
|
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
2021-12-20 21:04:02 +00:00
|
|
|
});
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await removeAttachmentDownloadJob(id);
|
|
|
|
delete _activeAttachmentDownloadJobs[id];
|
2022-12-21 18:41:48 +00:00
|
|
|
void _maybeStartJob();
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
function getActiveJobCount(): number {
|
2019-01-30 20:15:07 +00:00
|
|
|
return Object.keys(_activeAttachmentDownloadJobs).length;
|
|
|
|
}
|
|
|
|
|
2022-05-23 23:07:41 +00:00
|
|
|
function _markAttachmentAsPermanentError(
|
|
|
|
attachment: AttachmentType
|
|
|
|
): AttachmentType {
|
2019-01-30 20:15:07 +00:00
|
|
|
return {
|
2023-03-27 23:48:57 +00:00
|
|
|
...omit(attachment, ['key', 'id']),
|
2019-01-30 20:15:07 +00:00
|
|
|
error: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-30 16:24:28 +00:00
|
|
|
function _markAttachmentAsTooBig(attachment: AttachmentType): AttachmentType {
|
|
|
|
return {
|
|
|
|
...omit(attachment, ['key', 'id']),
|
|
|
|
error: true,
|
|
|
|
wasTooBig: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-05-23 23:07:41 +00:00
|
|
|
function _markAttachmentAsTransientError(
|
|
|
|
attachment: AttachmentType
|
|
|
|
): AttachmentType {
|
|
|
|
return { ...attachment, error: true };
|
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
async function _addAttachmentToMessage(
|
|
|
|
message: MessageModel | null | undefined,
|
|
|
|
attachment: AttachmentType,
|
|
|
|
{ type, index }: { type: AttachmentDownloadJobTypeType; index: number }
|
|
|
|
): Promise<void> {
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!message) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-22 01:16:37 +00:00
|
|
|
const logPrefix = `${message.idForLogging()} (type: ${type}, index: ${index})`;
|
2023-03-27 23:48:57 +00:00
|
|
|
const attachmentSignature = getAttachmentSignature(attachment);
|
2019-02-22 01:16:37 +00:00
|
|
|
|
2019-03-13 20:38:28 +00:00
|
|
|
if (type === 'long-message') {
|
2024-01-30 21:22:23 +00:00
|
|
|
let handledAnywhere = false;
|
|
|
|
let attachmentData: Uint8Array | undefined;
|
2022-05-23 23:07:41 +00:00
|
|
|
|
2019-03-13 20:38:28 +00:00
|
|
|
try {
|
2024-01-30 21:22:23 +00:00
|
|
|
if (attachment.path) {
|
|
|
|
const loaded = await window.Signal.Migrations.loadAttachmentData(
|
|
|
|
attachment
|
|
|
|
);
|
|
|
|
attachmentData = loaded.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
const editHistory = message.get('editHistory');
|
|
|
|
if (editHistory) {
|
|
|
|
let handledInEditHistory = false;
|
|
|
|
|
|
|
|
const newEditHistory = editHistory.map(edit => {
|
|
|
|
// We've already downloaded a bodyAttachment for this edit
|
|
|
|
if (!edit.bodyAttachment) {
|
|
|
|
return edit;
|
|
|
|
}
|
|
|
|
// This attachment isn't destined for this edit
|
|
|
|
if (
|
|
|
|
getAttachmentSignature(edit.bodyAttachment) !== attachmentSignature
|
|
|
|
) {
|
|
|
|
return edit;
|
|
|
|
}
|
|
|
|
|
|
|
|
handledInEditHistory = true;
|
|
|
|
handledAnywhere = true;
|
|
|
|
|
|
|
|
// Attachment wasn't downloaded yet.
|
|
|
|
if (!attachmentData) {
|
|
|
|
return {
|
|
|
|
...edit,
|
|
|
|
bodyAttachment: attachment,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...edit,
|
|
|
|
body: Bytes.toString(attachmentData),
|
|
|
|
bodyAttachment: undefined,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
if (handledInEditHistory) {
|
|
|
|
message.set({ editHistory: newEditHistory });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const existingBodyAttachment = message.get('bodyAttachment');
|
|
|
|
// A bodyAttachment download might apply only to an edit, and not the top-level
|
|
|
|
if (!existingBodyAttachment) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
getAttachmentSignature(existingBodyAttachment) !== attachmentSignature
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
handledAnywhere = true;
|
|
|
|
|
|
|
|
// Attachment wasn't downloaded yet.
|
|
|
|
if (!attachmentData) {
|
|
|
|
message.set({
|
|
|
|
bodyAttachment: attachment,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-13 20:38:28 +00:00
|
|
|
message.set({
|
2024-01-30 21:22:23 +00:00
|
|
|
body: Bytes.toString(attachmentData),
|
2022-05-23 23:07:41 +00:00
|
|
|
bodyAttachment: undefined,
|
2019-03-13 20:38:28 +00:00
|
|
|
});
|
|
|
|
} finally {
|
2021-06-17 17:15:10 +00:00
|
|
|
if (attachment.path) {
|
2024-01-30 21:22:23 +00:00
|
|
|
await window.Signal.Migrations.deleteAttachmentData(attachment.path);
|
|
|
|
}
|
|
|
|
if (!handledAnywhere) {
|
|
|
|
logger.warn(
|
|
|
|
`${logPrefix}: Long message attachment found no matching place to apply`
|
|
|
|
);
|
2021-06-17 17:15:10 +00:00
|
|
|
}
|
2019-03-13 20:38:28 +00:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
const maybeReplaceAttachment = (existing: AttachmentType): AttachmentType => {
|
|
|
|
if (isDownloaded(existing)) {
|
|
|
|
return existing;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attachmentSignature !== getAttachmentSignature(existing)) {
|
|
|
|
return existing;
|
|
|
|
}
|
|
|
|
|
|
|
|
return attachment;
|
|
|
|
};
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
if (type === 'attachment') {
|
|
|
|
const attachments = message.get('attachments');
|
2023-03-27 23:48:57 +00:00
|
|
|
|
2024-01-30 21:22:23 +00:00
|
|
|
let handledAnywhere = false;
|
2023-03-27 23:48:57 +00:00
|
|
|
let handledInEditHistory = false;
|
|
|
|
|
|
|
|
const editHistory = message.get('editHistory');
|
|
|
|
if (editHistory) {
|
|
|
|
const newEditHistory = editHistory.map(edit => {
|
|
|
|
if (!edit.attachments) {
|
|
|
|
return edit;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...edit,
|
|
|
|
// Loop through all the attachments to find the attachment we intend
|
|
|
|
// to replace.
|
2023-04-20 16:31:59 +00:00
|
|
|
attachments: edit.attachments.map(item => {
|
|
|
|
const newItem = maybeReplaceAttachment(item);
|
|
|
|
handledInEditHistory ||= item !== newItem;
|
2024-01-30 21:22:23 +00:00
|
|
|
handledAnywhere ||= handledInEditHistory;
|
2023-04-20 16:31:59 +00:00
|
|
|
return newItem;
|
2023-03-27 23:48:57 +00:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (handledInEditHistory) {
|
2023-03-27 23:48:57 +00:00
|
|
|
message.set({ editHistory: newEditHistory });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (attachments) {
|
|
|
|
message.set({
|
2024-01-30 21:22:23 +00:00
|
|
|
attachments: attachments.map(item => {
|
|
|
|
const newItem = maybeReplaceAttachment(item);
|
|
|
|
handledAnywhere ||= item !== newItem;
|
|
|
|
return newItem;
|
|
|
|
}),
|
2023-04-20 16:31:59 +00:00
|
|
|
});
|
2023-03-27 23:48:57 +00:00
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2024-01-30 21:22:23 +00:00
|
|
|
if (!handledAnywhere) {
|
|
|
|
logger.warn(
|
|
|
|
`${logPrefix}: 'attachment' type found no matching place to apply`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === 'preview') {
|
|
|
|
const preview = message.get('preview');
|
2023-03-27 23:48:57 +00:00
|
|
|
|
|
|
|
let handledInEditHistory = false;
|
|
|
|
|
|
|
|
const editHistory = message.get('editHistory');
|
|
|
|
if (preview && editHistory) {
|
|
|
|
const newEditHistory = editHistory.map(edit => {
|
2023-04-20 16:31:59 +00:00
|
|
|
if (!edit.preview) {
|
2023-03-27 23:48:57 +00:00
|
|
|
return edit;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...edit,
|
2023-04-20 16:31:59 +00:00
|
|
|
preview: edit.preview.map(item => {
|
|
|
|
if (!item.image) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newImage = maybeReplaceAttachment(item.image);
|
|
|
|
handledInEditHistory ||= item.image !== newImage;
|
|
|
|
return { ...item, image: newImage };
|
|
|
|
}),
|
2023-03-27 23:48:57 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (handledInEditHistory) {
|
2023-03-27 23:48:57 +00:00
|
|
|
message.set({ editHistory: newEditHistory });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (preview) {
|
|
|
|
message.set({
|
|
|
|
preview: preview.map(item => {
|
|
|
|
if (!item.image) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
image: maybeReplaceAttachment(item.image),
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
});
|
2023-03-27 23:48:57 +00:00
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === 'contact') {
|
|
|
|
const contact = message.get('contact');
|
|
|
|
if (!contact || contact.length <= index) {
|
|
|
|
throw new Error(
|
2024-01-30 21:22:23 +00:00
|
|
|
`${logPrefix}: contact didn't exist or ${index} was too large`
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
}
|
2023-04-20 16:31:59 +00:00
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
const item = contact[index];
|
|
|
|
if (item && item.avatar && item.avatar.avatar) {
|
2021-06-17 17:15:10 +00:00
|
|
|
_checkOldAttachment(item.avatar, 'avatar', logPrefix);
|
2019-09-04 00:07:47 +00:00
|
|
|
|
|
|
|
const newContact = [...contact];
|
|
|
|
newContact[index] = {
|
2021-06-17 17:15:10 +00:00
|
|
|
...item,
|
2019-09-04 00:07:47 +00:00
|
|
|
avatar: {
|
2021-06-17 17:15:10 +00:00
|
|
|
...item.avatar,
|
2019-09-04 00:07:47 +00:00
|
|
|
avatar: attachment,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
message.set({ contact: newContact });
|
2019-01-30 20:15:07 +00:00
|
|
|
} else {
|
|
|
|
logger.warn(
|
2024-01-30 21:22:23 +00:00
|
|
|
`${logPrefix}: Couldn't update contact with avatar attachment for message`
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === 'quote') {
|
|
|
|
const quote = message.get('quote');
|
2023-04-20 16:31:59 +00:00
|
|
|
const editHistory = message.get('editHistory');
|
|
|
|
let handledInEditHistory = false;
|
|
|
|
if (editHistory) {
|
|
|
|
const newEditHistory = editHistory.map(edit => {
|
|
|
|
if (!edit.quote) {
|
|
|
|
return edit;
|
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
return {
|
|
|
|
...edit,
|
|
|
|
quote: {
|
|
|
|
...edit.quote,
|
|
|
|
attachments: edit.quote.attachments.map(item => {
|
|
|
|
const { thumbnail } = item;
|
|
|
|
if (!thumbnail) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newThumbnail = maybeReplaceAttachment(thumbnail);
|
|
|
|
if (thumbnail !== newThumbnail) {
|
|
|
|
handledInEditHistory = true;
|
|
|
|
}
|
|
|
|
return { ...item, thumbnail: newThumbnail };
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (handledInEditHistory) {
|
|
|
|
message.set({ editHistory: newEditHistory });
|
|
|
|
}
|
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
if (quote) {
|
|
|
|
const newQuote = {
|
|
|
|
...quote,
|
|
|
|
attachments: quote.attachments.map(item => {
|
|
|
|
const { thumbnail } = item;
|
|
|
|
if (!thumbnail) {
|
|
|
|
return item;
|
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
thumbnail: maybeReplaceAttachment(thumbnail),
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
};
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2023-04-20 16:31:59 +00:00
|
|
|
message.set({ quote: newQuote });
|
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-05-16 22:32:11 +00:00
|
|
|
if (type === 'sticker') {
|
|
|
|
const sticker = message.get('sticker');
|
|
|
|
if (!sticker) {
|
2024-01-30 21:22:23 +00:00
|
|
|
throw new Error(`${logPrefix}: sticker didn't exist`);
|
2019-05-16 22:32:11 +00:00
|
|
|
}
|
|
|
|
|
2019-08-22 22:04:14 +00:00
|
|
|
message.set({
|
|
|
|
sticker: {
|
|
|
|
...sticker,
|
|
|
|
data: attachment,
|
|
|
|
},
|
|
|
|
});
|
2019-01-30 20:15:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-01-30 21:22:23 +00:00
|
|
|
throw new Error(`${logPrefix}: Unknown job type ${type}`);
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
function _checkOldAttachment(
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
object: any,
|
|
|
|
key: string,
|
|
|
|
logPrefix: string
|
|
|
|
): void {
|
2019-01-30 20:15:07 +00:00
|
|
|
const oldAttachment = object[key];
|
|
|
|
if (oldAttachment && oldAttachment.path) {
|
2019-09-04 00:07:47 +00:00
|
|
|
logger.error(
|
|
|
|
`_checkOldAttachment: ${logPrefix} - old attachment already had path, not replacing`
|
|
|
|
);
|
|
|
|
throw new Error(
|
|
|
|
'_checkOldAttachment: old attachment already had path, not replacing'
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|