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
|
2024-04-16 00:11:48 +00:00
|
|
|
import * as log from '../logging/log';
|
2021-09-24 00:49:05 +00:00
|
|
|
import * as Bytes from '../Bytes';
|
2024-04-16 00:11:48 +00:00
|
|
|
import type { AttachmentDownloadJobTypeType } from '../types/AttachmentDownload';
|
2021-06-17 17:15:10 +00:00
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { MessageModel } from '../models/messages';
|
|
|
|
import type { AttachmentType } from '../types/Attachment';
|
2024-04-16 00:11:48 +00:00
|
|
|
import { getAttachmentSignature, isDownloaded } from '../types/Attachment';
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2024-04-16 00:11:48 +00:00
|
|
|
export async function addAttachmentToMessage(
|
2021-06-17 17:15:10 +00:00
|
|
|
message: MessageModel | null | undefined,
|
|
|
|
attachment: AttachmentType,
|
2024-04-19 17:09:51 +00:00
|
|
|
jobLogId: string,
|
2024-04-16 00:11:48 +00:00
|
|
|
{ type }: { type: AttachmentDownloadJobTypeType }
|
2021-06-17 17:15:10 +00:00
|
|
|
): Promise<void> {
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!message) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-04-19 17:09:51 +00:00
|
|
|
const logPrefix = `${jobLogId}/addAttachmentToMessage`;
|
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) {
|
2024-04-16 00:11:48 +00:00
|
|
|
log.warn(
|
2024-01-30 21:22:23 +00:00
|
|
|
`${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) {
|
2024-04-16 00:11:48 +00:00
|
|
|
log.warn(
|
2024-01-30 21:22:23 +00:00
|
|
|
`${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') {
|
2024-04-16 00:11:48 +00:00
|
|
|
const contacts = message.get('contact');
|
|
|
|
if (!contacts?.length) {
|
|
|
|
throw new Error(`${logPrefix}: no contacts, cannot add attachment!`);
|
2019-01-30 20:15:07 +00:00
|
|
|
}
|
2024-04-16 00:11:48 +00:00
|
|
|
let handled = false;
|
2023-04-20 16:31:59 +00:00
|
|
|
|
2024-04-16 00:11:48 +00:00
|
|
|
const newContacts = contacts.map(contact => {
|
|
|
|
if (!contact.avatar?.avatar) {
|
|
|
|
return contact;
|
|
|
|
}
|
2019-09-04 00:07:47 +00:00
|
|
|
|
2024-04-16 00:11:48 +00:00
|
|
|
const existingAttachment = contact.avatar.avatar;
|
|
|
|
|
|
|
|
const newAttachment = maybeReplaceAttachment(existingAttachment);
|
|
|
|
if (existingAttachment !== newAttachment) {
|
|
|
|
handled = true;
|
|
|
|
return {
|
|
|
|
...contact,
|
|
|
|
avatar: { ...contact.avatar, avatar: newAttachment },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return contact;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!handled) {
|
|
|
|
throw new Error(
|
|
|
|
`${logPrefix}: Couldn't find matching contact with avatar attachment for message`
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-16 00:11:48 +00:00
|
|
|
message.set({ contact: newContacts });
|
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
|
|
|
}
|