Normalize message attachments

This commit is contained in:
trevor-signal
2025-05-22 21:09:54 -04:00
committed by GitHub
parent 8d8e0329cf
commit d6e81eee11
39 changed files with 2540 additions and 807 deletions

View File

@@ -24,7 +24,11 @@ import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../util/GoogleChrome';
import type { LocalizerType, WithRequiredProperties } from './Util';
import type {
LocalizerType,
WithOptionalProperties,
WithRequiredProperties,
} from './Util';
import { ThemeType } from './Util';
import * as GoogleChrome from '../util/GoogleChrome';
import { ReadStatus } from '../messages/MessageReadStatus';
@@ -56,14 +60,47 @@ export class AttachmentPermanentlyUndownloadableError extends Error {
}
}
type ScreenshotType = Omit<AttachmentType, 'size'> & {
height: number;
width: number;
path: string;
size?: number;
export type ThumbnailType = EphemeralAttachmentFields & {
size: number;
contentType: MIME.MIMEType;
path?: string;
plaintextHash?: string;
width?: number;
height?: number;
version?: 1 | 2;
localKey?: string; // AES + MAC
};
export type AttachmentType = {
export type ScreenshotType = WithOptionalProperties<ThumbnailType, 'size'>;
export type BackupThumbnailType = WithOptionalProperties<ThumbnailType, 'size'>;
// These fields do not get saved to the DB.
export type EphemeralAttachmentFields = {
totalDownloaded?: number;
data?: Uint8Array;
/** Not included in protobuf, needs to be pulled from flags */
isVoiceMessage?: boolean;
/** For messages not already on disk, this will be a data url */
url?: string;
screenshotData?: Uint8Array;
/** @deprecated Legacy field */
screenshotPath?: string;
/** @deprecated Legacy field. Used only for downloading old attachment */
id?: number;
/** @deprecated Legacy field, used long ago for migrating attachments to disk. */
schemaVersion?: number;
/** @deprecated Legacy field, replaced by cdnKey */
cdnId?: string;
};
/**
* Adding a field to AttachmentType requires:
* 1) adding a column to message_attachments
* 2) updating MessageAttachmentDBReferenceType and MESSAGE_ATTACHMENT_COLUMNS
* 3) saving data to the proper column
*/
export type AttachmentType = EphemeralAttachmentFields & {
error?: boolean;
blurHash?: string;
caption?: string;
@@ -73,36 +110,27 @@ export type AttachmentType = {
fileName?: string;
plaintextHash?: string;
uploadTimestamp?: number;
/** Not included in protobuf, needs to be pulled from flags */
isVoiceMessage?: boolean;
/** For messages not already on disk, this will be a data url */
url?: string;
size: number;
pending?: boolean;
width?: number;
height?: number;
path?: string;
screenshot?: ScreenshotType;
screenshotData?: Uint8Array;
// Legacy Draft
screenshotPath?: string;
flags?: number;
thumbnail?: ThumbnailType;
isCorrupted?: boolean;
cdnNumber?: number;
cdnId?: string;
cdnKey?: string;
downloadPath?: string;
key?: string;
iv?: string;
data?: Uint8Array;
textAttachment?: TextAttachmentType;
wasTooBig?: boolean;
// If `true` backfill is unavailable
backfillError?: boolean;
totalDownloaded?: number;
incrementalMac?: string;
chunkSize?: number;
@@ -115,25 +143,19 @@ export type AttachmentType = {
// See app/attachment_channel.ts
version?: 1 | 2;
localKey?: string; // AES + MAC
thumbnailFromBackup?: Pick<
AttachmentType,
'path' | 'version' | 'plaintextHash'
>;
thumbnailFromBackup?: BackupThumbnailType;
/** Legacy field. Used only for downloading old attachments */
id?: number;
/** Legacy field, used long ago for migrating attachments to disk. */
schemaVersion?: number;
/** For quote attachments, if copied from the referenced attachment */
copied?: boolean;
} & (
| {
isReencryptableToSameDigest?: true;
}
| {
isReencryptableToSameDigest: false;
reencryptionInfo?: ReencryptionInfo;
}
);
| {
isReencryptableToSameDigest?: true;
}
| {
isReencryptableToSameDigest: false;
reencryptionInfo?: ReencryptionInfo;
}
);
export type LocalAttachmentV2Type = Readonly<{
version: 2;
@@ -259,13 +281,6 @@ export type AttachmentDraftType =
size: number;
};
export type ThumbnailType = AttachmentType & {
// Only used when quote needed to make an in-memory thumbnail
objectUrl?: string;
// Whether the thumbnail has been copied from the original (quoted) message
copied?: boolean;
};
export enum AttachmentVariant {
Default = 'Default',
ThumbnailFromBackup = 'thumbnailFromBackup',
@@ -1008,6 +1023,10 @@ export const isFile = (attachment: AttachmentType): boolean => {
return false;
}
if (MIME.isLongMessage(contentType)) {
return false;
}
return true;
};

View File

@@ -4,7 +4,6 @@
import type { DurationInSeconds } from '../util/durations';
import type { AttachmentType } from './Attachment';
import type { EmbeddedContactType } from './EmbeddedContact';
import type { IndexableBoolean, IndexablePresence } from './IndexedDB';
export function getMentionsRegex(): RegExp {
return /\uFFFC/g;
@@ -34,7 +33,6 @@ export type IncomingMessage = Readonly<
source?: string;
sourceDevice?: number;
} & SharedMessageProperties &
MessageSchemaVersion5 &
MessageSchemaVersion6 &
ExpirationTimerUpdate
>;
@@ -56,7 +54,6 @@ export type OutgoingMessage = Readonly<
isViewOnce?: number;
synced: boolean;
} & SharedMessageProperties &
MessageSchemaVersion5 &
ExpirationTimerUpdate
>;
@@ -64,7 +61,6 @@ export type VerifiedChangeMessage = Readonly<
{
type: 'verified-change';
} & SharedMessageProperties &
MessageSchemaVersion5 &
ExpirationTimerUpdate
>;
@@ -72,7 +68,6 @@ export type ProfileChangeNotificationMessage = Readonly<
{
type: 'profile-change';
} & SharedMessageProperties &
MessageSchemaVersion5 &
ExpirationTimerUpdate
>;
@@ -92,14 +87,6 @@ export type ExpirationTimerUpdate = Partial<
}>
>;
export type MessageSchemaVersion5 = Partial<
Readonly<{
hasAttachments: IndexableBoolean;
hasVisualMediaAttachments: IndexablePresence;
hasFileAttachments: IndexablePresence;
}>
>;
export type MessageSchemaVersion6 = Partial<
Readonly<{
contact: Array<EmbeddedContactType>;

View File

@@ -23,7 +23,6 @@ import {
} from './Attachment';
import * as Errors from './errors';
import * as SchemaVersion from './SchemaVersion';
import { initializeAttachmentMetadata } from './message/initializeAttachmentMetadata';
import { LONG_MESSAGE } from './MIME';
import type * as MIME from './MIME';
@@ -140,6 +139,8 @@ export type ContextType = {
// - Attachments: write bodyAttachment to disk
// Version 14
// - All attachments: ensure they are reencryptable to a known digest
// Version 15
// - A noop migration to cause attachments to be normalized when the message is saved
const INITIAL_SCHEMA_VERSION = 0;
@@ -488,12 +489,10 @@ const toVersion6 = _withSchemaVersion({
schemaVersion: 6,
upgrade: _mapContact(Contact.parseAndWriteAvatar(migrateDataToFileSystem)),
});
// IMPORTANT: Weve updated our definition of `initializeAttachmentMetadata`, so
// we need to run it again on existing items that have previously been incorrectly
// classified:
// NOOP: hasFileAttachments, etc. is now computed at message save time
const toVersion7 = _withSchemaVersion({
schemaVersion: 7,
upgrade: initializeAttachmentMetadata,
upgrade: noopUpgrade,
});
const toVersion8 = _withSchemaVersion({
@@ -655,6 +654,7 @@ const toVersion12 = _withSchemaVersion({
return result;
},
});
const toVersion13 = _withSchemaVersion({
schemaVersion: 13,
upgrade: migrateBodyAttachmentToDisk,

View File

@@ -31,6 +31,7 @@ import { drop } from '../util/drop';
import { isNotNil } from '../util/isNotNil';
import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment';
import { AttachmentDisposition } from '../util/getLocalAttachmentUrl';
import { getPlaintextHashForInMemoryAttachment } from '../AttachmentCrypto';
export type ActionSourceType =
| 'startup'
@@ -1094,7 +1095,6 @@ export async function copyStickerToAttachments(
// Fall-back
contentType: IMAGE_WEBP,
};
const data = await window.Signal.Migrations.readAttachmentData(newSticker);
const sniffedMimeType = sniffImageMimeType(data);
@@ -1106,6 +1106,8 @@ export async function copyStickerToAttachments(
);
}
newSticker.plaintextHash = getPlaintextHashForInMemoryAttachment(data);
return newSticker;
}

View File

@@ -113,6 +113,9 @@ export type JSONWithUnknownFields<Value> =
export type WithRequiredProperties<T, P extends keyof T> = Omit<T, P> &
Required<Pick<T, P>>;
export type WithOptionalProperties<T, P extends keyof T> = Omit<T, P> &
Partial<Pick<T, P>>;
export function getTypingIndicatorSetting(): boolean {
return window.storage.get('typingIndicators', false);
}

View File

@@ -1,48 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as Attachment from '../Attachment';
import * as IndexedDB from '../IndexedDB';
import type { MessageAttributesType } from '../../model-types.d';
const hasAttachment =
(predicate: (value: Attachment.AttachmentType) => boolean) =>
(message: MessageAttributesType): IndexedDB.IndexablePresence =>
IndexedDB.toIndexablePresence((message.attachments || []).some(predicate));
const hasFileAttachment = hasAttachment(Attachment.isFile);
const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia);
export const initializeAttachmentMetadata = async (
message: MessageAttributesType
): Promise<MessageAttributesType> => {
if (message.type === 'verified-change') {
return message;
}
if (message.type === 'profile-change') {
return message;
}
if (message.messageTimer || message.isViewOnce) {
return message;
}
const attachments = (message.attachments || []).filter(
(attachment: Attachment.AttachmentType) =>
attachment.contentType !== 'text/x-signal-plain'
);
const hasAttachments = IndexedDB.toIndexableBoolean(attachments.length > 0);
const hasFileAttachments = hasFileAttachment({ ...message, attachments });
const hasVisualMediaAttachments = hasVisualMediaAttachment({
...message,
attachments,
});
return {
...message,
hasAttachments,
hasFileAttachments,
hasVisualMediaAttachments,
};
};