Re-use standard attachments on edit

This commit is contained in:
trevor-signal
2025-10-14 15:55:26 -04:00
committed by GitHub
parent 531d1ffac4
commit 512eccda88
5 changed files with 98 additions and 19 deletions

View File

@@ -4,6 +4,7 @@
import lodash from 'lodash'; import lodash from 'lodash';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { ContentHint } from '@signalapp/libsignal-client'; import { ContentHint } from '@signalapp/libsignal-client';
import Long from 'long';
import * as Errors from '../../types/errors.js'; import * as Errors from '../../types/errors.js';
import { strictAssert } from '../../util/assert.js'; import { strictAssert } from '../../util/assert.js';
@@ -33,6 +34,7 @@ import type {
OutgoingStickerType, OutgoingStickerType,
} from '../../textsecure/SendMessage.js'; } from '../../textsecure/SendMessage.js';
import type { import type {
AttachmentDownloadableFromTransitTier,
AttachmentType, AttachmentType,
UploadedAttachmentType, UploadedAttachmentType,
} from '../../types/Attachment.js'; } from '../../types/Attachment.js';
@@ -74,6 +76,11 @@ import {
} from '../../test-node/util/messageFailures.js'; } from '../../test-node/util/messageFailures.js';
import { getMessageIdForLogging } from '../../util/idForLogging.js'; import { getMessageIdForLogging } from '../../util/idForLogging.js';
import { send, sendSyncMessageOnly } from '../../messages/send.js'; import { send, sendSyncMessageOnly } from '../../messages/send.js';
import type { SignalService } from '../../protobuf/index.js';
import { uuidToBytes } from '../../util/uuidToBytes.js';
import { fromBase64 } from '../../Bytes.js';
import { MIMETypeToString } from '../../types/MIME.js';
import { canReuseExistingTransitCdnPointerForEditedMessage } from '../../util/Attachment.js';
const { isNumber } = lodash; const { isNumber } = lodash;
@@ -220,7 +227,12 @@ export async function sendNormalMessage(
sticker, sticker,
storyMessage, storyMessage,
storyContext, storyContext,
} = await getMessageSendData({ log, message, targetTimestamp }); } = await getMessageSendData({
log,
message,
targetTimestamp,
isEditedMessageSend: editedMessageTimestamp != null,
});
if (reaction) { if (reaction) {
strictAssert( strictAssert(
@@ -584,12 +596,14 @@ async function getMessageSendData({
log, log,
message, message,
targetTimestamp, targetTimestamp,
isEditedMessageSend,
}: Readonly<{ }: Readonly<{
log: LoggerType; log: LoggerType;
message: MessageModel; message: MessageModel;
targetTimestamp: number; targetTimestamp: number;
isEditedMessageSend: boolean;
}>): Promise<{ }>): Promise<{
attachments: Array<UploadedAttachmentType>; attachments: Array<SignalService.IAttachmentPointer>;
body: undefined | string; body: undefined | string;
contact?: Array<EmbeddedContactWithUploadedAvatar>; contact?: Array<EmbeddedContactWithUploadedAvatar>;
deletedForEveryoneTimestamp: undefined | number; deletedForEveryoneTimestamp: undefined | number;
@@ -652,15 +666,21 @@ async function getMessageSendData({
storyMessage, storyMessage,
] = await Promise.all([ ] = await Promise.all([
uploadQueue.addAll( uploadQueue.addAll(
preUploadAttachments.map( preUploadAttachments.map(attachment => async () => {
attachment => () => if (isEditedMessageSend) {
uploadSingleAttachment({ if (canReuseExistingTransitCdnPointerForEditedMessage(attachment)) {
attachment, return convertAttachmentToPointer(attachment);
log, }
message, log.error('Unable to reuse attachment pointer for edited message');
targetTimestamp, }
})
) return uploadSingleAttachment({
attachment,
log,
message,
targetTimestamp,
});
})
), ),
uploadQueue.add(async () => uploadQueue.add(async () =>
maybeLongAttachment maybeLongAttachment
@@ -1258,3 +1278,47 @@ function didSendToEveryone({
} }
); );
} }
function convertAttachmentToPointer(
attachment: AttachmentDownloadableFromTransitTier
): SignalService.IAttachmentPointer {
const {
cdnKey,
cdnNumber,
clientUuid,
key,
size,
digest,
incrementalMac,
chunkSize,
uploadTimestamp,
contentType,
fileName,
flags,
width,
height,
caption,
blurHash,
} = attachment;
return {
cdnKey,
cdnNumber,
clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined,
key: fromBase64(key),
size,
digest: fromBase64(digest),
incrementalMac: incrementalMac ? fromBase64(incrementalMac) : undefined,
chunkSize,
uploadTimestamp: uploadTimestamp
? Long.fromNumber(uploadTimestamp)
: undefined,
contentType: MIMETypeToString(contentType),
fileName,
flags,
width,
height,
caption,
blurHash,
};
}

View File

@@ -189,7 +189,7 @@ export const singleProtoJobDataSchema = z.object({
export type SingleProtoJobData = z.infer<typeof singleProtoJobDataSchema>; export type SingleProtoJobData = z.infer<typeof singleProtoJobDataSchema>;
export type MessageOptionsType = { export type MessageOptionsType = {
attachments?: ReadonlyArray<UploadedAttachmentType>; attachments?: ReadonlyArray<Proto.IAttachmentPointer>;
body?: string; body?: string;
bodyRanges?: ReadonlyArray<RawBodyRange>; bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>; contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
@@ -215,7 +215,7 @@ export type MessageOptionsType = {
storyContext?: StoryContextType; storyContext?: StoryContextType;
}; };
export type GroupSendOptionsType = { export type GroupSendOptionsType = {
attachments?: ReadonlyArray<UploadedAttachmentType>; attachments?: ReadonlyArray<Proto.IAttachmentPointer>;
bodyRanges?: ReadonlyArray<RawBodyRange>; bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>; contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
deletedForEveryoneTimestamp?: number; deletedForEveryoneTimestamp?: number;
@@ -235,7 +235,7 @@ export type GroupSendOptionsType = {
}; };
class Message { class Message {
attachments: ReadonlyArray<UploadedAttachmentType>; attachments: ReadonlyArray<Proto.IAttachmentPointer>;
body?: string; body?: string;
@@ -1211,7 +1211,7 @@ export class MessageSender {
urgent, urgent,
includePniSignatureMessage, includePniSignatureMessage,
}: Readonly<{ }: Readonly<{
attachments: ReadonlyArray<UploadedAttachmentType> | undefined; attachments: ReadonlyArray<Proto.IAttachmentPointer> | undefined;
bodyRanges?: ReadonlyArray<RawBodyRange>; bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>; contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
contentHint: number; contentHint: number;

View File

@@ -127,13 +127,11 @@ export type UploadedAttachmentType = Proto.IAttachmentPointer &
Readonly<{ Readonly<{
// Required fields // Required fields
cdnKey: string; cdnKey: string;
iv: Uint8Array;
key: Uint8Array; key: Uint8Array;
size: number; size: number;
digest: Uint8Array; digest: Uint8Array;
contentType: string; contentType: string;
plaintextHash: string; plaintextHash: string;
isReencryptableToSameDigest: true;
}>; }>;
export type AttachmentWithHydratedData = AttachmentType & { export type AttachmentWithHydratedData = AttachmentType & {

View File

@@ -60,6 +60,8 @@ const MIN_TIMELINE_IMAGE_HEIGHT = 50;
const MAX_DISPLAYABLE_IMAGE_WIDTH = 8192; const MAX_DISPLAYABLE_IMAGE_WIDTH = 8192;
const MAX_DISPLAYABLE_IMAGE_HEIGHT = 8192; const MAX_DISPLAYABLE_IMAGE_HEIGHT = 8192;
const MAX_DURATION_TO_REUSE_ATTACHMENT_CDN_POINTER = 3 * DAY;
// // Incoming message attachment fields // // Incoming message attachment fields
// { // {
// id: string // id: string
@@ -1173,6 +1175,23 @@ export function partitionBodyAndNormalAttachments<
}; };
} }
export function canReuseExistingTransitCdnPointerForEditedMessage(
attachment: AttachmentType
): attachment is AttachmentDownloadableFromTransitTier {
// In practice, this should always return true, since the timeframe for editing a
// message is less than MAX_DURATION_TO_REUSE_ATTACHMENT_CDN_POINTER
return (
isValidDigest(attachment.digest) &&
isValidAttachmentKey(attachment.key) &&
attachment.cdnKey != null &&
attachment.cdnNumber != null &&
isMoreRecentThan(
attachment.uploadTimestamp ?? 0,
MAX_DURATION_TO_REUSE_ATTACHMENT_CDN_POINTER
)
);
}
const MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS: Set<MessageAttachmentType> = const MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS: Set<MessageAttachmentType> =
new Set(['attachment', 'sticker']); new Set(['attachment', 'sticker']);

View File

@@ -54,7 +54,6 @@ export async function uploadAttachment(
cdnNumber, cdnNumber,
clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined, clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined,
key: keys, key: keys,
iv: encrypted.iv,
size: attachment.data.byteLength, size: attachment.data.byteLength,
digest: encrypted.digest, digest: encrypted.digest,
plaintextHash: encrypted.plaintextHash, plaintextHash: encrypted.plaintextHash,
@@ -69,7 +68,6 @@ export async function uploadAttachment(
height, height,
caption, caption,
blurHash, blurHash,
isReencryptableToSameDigest: true,
}; };
} }