Support voice memo backups

This commit is contained in:
trevor-signal 2024-06-06 12:16:27 -04:00 committed by GitHub
parent 348503c7f9
commit 9cbbbe0ef0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 82 additions and 19 deletions

View file

@ -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<Backups.IStandardMessage> {
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 => {

View file

@ -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

View file

@ -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

View file

@ -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', () => {