diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index f6467c5421..a1c1720378 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -87,10 +87,10 @@ import { numberToPhoneType, } from '../../types/EmbeddedContact'; import { - isVoiceMessage, type AttachmentType, isGIF, isDownloaded, + isVoiceMessage as isVoiceMessageAttachment, } from '../../types/Attachment'; import { getFilePointerForAttachment, @@ -1692,7 +1692,7 @@ export class BackupExportStream extends Readable { private getMessageAttachmentFlag( attachment: AttachmentType ): Backups.MessageAttachment.Flag { - if (isVoiceMessage(attachment)) { + if (isVoiceMessageAttachment(attachment)) { return Backups.MessageAttachment.Flag.VOICE_MESSAGE; } if (isGIF([attachment])) { @@ -1876,6 +1876,9 @@ export class BackupExportStream extends Readable { >, backupLevel: BackupLevel ): Promise { + const isVoiceMessage = message.attachments?.some(isVoiceMessageAttachment); + const includeText = !isVoiceMessage; + return { quote: await this.toQuote(message.quote), attachments: message.attachments @@ -1889,13 +1892,17 @@ export class BackupExportStream extends Readable { }) ) : undefined, - text: { - // Note that we store full text on the message model so we have to - // trim it before serializing. - body: message.body?.slice(0, LONG_ATTACHMENT_LIMIT), - bodyRanges: message.bodyRanges?.map(range => this.toBodyRange(range)), - }, - + text: includeText + ? { + // TODO (DESKTOP-7207): handle long message text attachments + // Note that we store full text on the message model so we have to + // trim it before serializing. + body: message.body?.slice(0, LONG_ATTACHMENT_LIMIT), + bodyRanges: message.bodyRanges?.map(range => + this.toBodyRange(range) + ), + } + : undefined, linkPreview: message.preview ? await Promise.all( message.preview.map(async preview => { diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index abd065f5e4..7c0eb640e2 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -58,7 +58,10 @@ import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads'; import { drop } from '../../util/drop'; import { isNotNil } from '../../util/isNotNil'; import { isGroup } from '../../util/whatTypeOfConversation'; -import { convertFilePointerToAttachment } from './util/filePointers'; +import { + convertBackupMessageAttachmentToAttachment, + convertFilePointerToAttachment, +} from './util/filePointers'; const MAX_CONCURRENCY = 10; @@ -976,12 +979,7 @@ export class BackupImportStream extends Writable { body: data.text?.body || undefined, attachments: data.attachments?.length ? data.attachments - .map(attachment => { - if (!attachment.pointer) { - return null; - } - return convertFilePointerToAttachment(attachment.pointer); - }) + .map(convertBackupMessageAttachmentToAttachment) .filter(isNotNil) : undefined, preview: data.linkPreview?.length diff --git a/ts/services/backups/util/filePointers.ts b/ts/services/backups/util/filePointers.ts index 0ec04f0b64..0edb48558c 100644 --- a/ts/services/backups/util/filePointers.ts +++ b/ts/services/backups/util/filePointers.ts @@ -20,7 +20,7 @@ import { isDecryptable, isReencryptableToSameDigest, } from '../../../types/Attachment'; -import { Backups } from '../../../protobuf'; +import { Backups, SignalService } from '../../../protobuf'; import * as Bytes from '../../../Bytes'; import { getTimestampFromLong } from '../../../util/timestampLongUtils'; import { @@ -36,6 +36,7 @@ import { getMediaNameForAttachment, } from './mediaId'; import { redactGenericText } from '../../../util/privacy'; +import { missingCaseError } from '../../../util/missingCaseError'; export function convertFilePointerToAttachment( filePointer: Backups.FilePointer @@ -123,6 +124,35 @@ export function convertFilePointerToAttachment( throw new Error('convertFilePointerToAttachment: mising locator'); } +export function convertBackupMessageAttachmentToAttachment( + messageAttachment: Backups.IMessageAttachment +): AttachmentType | null { + if (!messageAttachment.pointer) { + return null; + } + const result = convertFilePointerToAttachment(messageAttachment.pointer); + switch (messageAttachment.flag) { + case Backups.MessageAttachment.Flag.VOICE_MESSAGE: + result.flags = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; + break; + case Backups.MessageAttachment.Flag.BORDERLESS: + result.flags = SignalService.AttachmentPointer.Flags.BORDERLESS; + break; + case Backups.MessageAttachment.Flag.GIF: + result.flags = SignalService.AttachmentPointer.Flags.GIF; + break; + case Backups.MessageAttachment.Flag.NONE: + case null: + case undefined: + result.flags = undefined; + break; + default: + throw missingCaseError(messageAttachment.flag); + } + + return result; +} + /** * Some attachments saved on desktop do not include the key used to encrypt the file * originally. This means that we need to encrypt the file in-memory now (at diff --git a/ts/test-electron/backup/attachments_test.ts b/ts/test-electron/backup/attachments_test.ts index 5f29427230..7e75ae7543 100644 --- a/ts/test-electron/backup/attachments_test.ts +++ b/ts/test-electron/backup/attachments_test.ts @@ -13,10 +13,11 @@ import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; import { loadCallsHistory } from '../../services/callHistoryLoader'; import { setupBasics, asymmetricRoundtripHarness } from './helpers'; -import { IMAGE_JPEG } from '../../types/MIME'; +import { AUDIO_MP3, IMAGE_JPEG } from '../../types/MIME'; import type { MessageAttributesType } from '../../model-types'; -import type { AttachmentType } from '../../types/Attachment'; +import { isVoiceMessage, type AttachmentType } from '../../types/Attachment'; import { strictAssert } from '../../util/assert'; +import { SignalService } from '../../protobuf'; const CONTACT_A = generateAci(); @@ -129,6 +130,33 @@ describe('backup/attachments', () => { BackupLevel.Media ); }); + it('roundtrips voice message attachments', async () => { + const attachment = composeAttachment(1); + attachment.contentType = AUDIO_MP3; + attachment.flags = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; + + strictAssert(isVoiceMessage(attachment), 'it is a voice attachment'); + strictAssert(attachment.digest, 'digest exists'); + + await asymmetricRoundtripHarness( + [ + composeMessage(1, { + attachments: [attachment], + }), + ], + [ + composeMessage(1, { + attachments: [ + { + ...omit(attachment, ['path', 'iv', 'uploadTimestamp']), + backupLocator: { mediaName: attachment.digest }, + }, + ], + }), + ], + BackupLevel.Media + ); + }); }); describe('Preview attachments', () => {