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, numberToPhoneType,
} from '../../types/EmbeddedContact'; } from '../../types/EmbeddedContact';
import { import {
isVoiceMessage,
type AttachmentType, type AttachmentType,
isGIF, isGIF,
isDownloaded, isDownloaded,
isVoiceMessage as isVoiceMessageAttachment,
} from '../../types/Attachment'; } from '../../types/Attachment';
import { import {
getFilePointerForAttachment, getFilePointerForAttachment,
@ -1692,7 +1692,7 @@ export class BackupExportStream extends Readable {
private getMessageAttachmentFlag( private getMessageAttachmentFlag(
attachment: AttachmentType attachment: AttachmentType
): Backups.MessageAttachment.Flag { ): Backups.MessageAttachment.Flag {
if (isVoiceMessage(attachment)) { if (isVoiceMessageAttachment(attachment)) {
return Backups.MessageAttachment.Flag.VOICE_MESSAGE; return Backups.MessageAttachment.Flag.VOICE_MESSAGE;
} }
if (isGIF([attachment])) { if (isGIF([attachment])) {
@ -1876,6 +1876,9 @@ export class BackupExportStream extends Readable {
>, >,
backupLevel: BackupLevel backupLevel: BackupLevel
): Promise<Backups.IStandardMessage> { ): Promise<Backups.IStandardMessage> {
const isVoiceMessage = message.attachments?.some(isVoiceMessageAttachment);
const includeText = !isVoiceMessage;
return { return {
quote: await this.toQuote(message.quote), quote: await this.toQuote(message.quote),
attachments: message.attachments attachments: message.attachments
@ -1889,13 +1892,17 @@ export class BackupExportStream extends Readable {
}) })
) )
: undefined, : undefined,
text: { text: includeText
// Note that we store full text on the message model so we have to ? {
// trim it before serializing. // TODO (DESKTOP-7207): handle long message text attachments
body: message.body?.slice(0, LONG_ATTACHMENT_LIMIT), // Note that we store full text on the message model so we have to
bodyRanges: message.bodyRanges?.map(range => this.toBodyRange(range)), // trim it before serializing.
}, body: message.body?.slice(0, LONG_ATTACHMENT_LIMIT),
bodyRanges: message.bodyRanges?.map(range =>
this.toBodyRange(range)
),
}
: undefined,
linkPreview: message.preview linkPreview: message.preview
? await Promise.all( ? await Promise.all(
message.preview.map(async preview => { message.preview.map(async preview => {

View file

@ -58,7 +58,10 @@ import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { drop } from '../../util/drop'; import { drop } from '../../util/drop';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
import { isGroup } from '../../util/whatTypeOfConversation'; import { isGroup } from '../../util/whatTypeOfConversation';
import { convertFilePointerToAttachment } from './util/filePointers'; import {
convertBackupMessageAttachmentToAttachment,
convertFilePointerToAttachment,
} from './util/filePointers';
const MAX_CONCURRENCY = 10; const MAX_CONCURRENCY = 10;
@ -976,12 +979,7 @@ export class BackupImportStream extends Writable {
body: data.text?.body || undefined, body: data.text?.body || undefined,
attachments: data.attachments?.length attachments: data.attachments?.length
? data.attachments ? data.attachments
.map(attachment => { .map(convertBackupMessageAttachmentToAttachment)
if (!attachment.pointer) {
return null;
}
return convertFilePointerToAttachment(attachment.pointer);
})
.filter(isNotNil) .filter(isNotNil)
: undefined, : undefined,
preview: data.linkPreview?.length preview: data.linkPreview?.length

View file

@ -20,7 +20,7 @@ import {
isDecryptable, isDecryptable,
isReencryptableToSameDigest, isReencryptableToSameDigest,
} from '../../../types/Attachment'; } from '../../../types/Attachment';
import { Backups } from '../../../protobuf'; import { Backups, SignalService } from '../../../protobuf';
import * as Bytes from '../../../Bytes'; import * as Bytes from '../../../Bytes';
import { getTimestampFromLong } from '../../../util/timestampLongUtils'; import { getTimestampFromLong } from '../../../util/timestampLongUtils';
import { import {
@ -36,6 +36,7 @@ import {
getMediaNameForAttachment, getMediaNameForAttachment,
} from './mediaId'; } from './mediaId';
import { redactGenericText } from '../../../util/privacy'; import { redactGenericText } from '../../../util/privacy';
import { missingCaseError } from '../../../util/missingCaseError';
export function convertFilePointerToAttachment( export function convertFilePointerToAttachment(
filePointer: Backups.FilePointer filePointer: Backups.FilePointer
@ -123,6 +124,35 @@ export function convertFilePointerToAttachment(
throw new Error('convertFilePointerToAttachment: mising locator'); 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 * 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 * 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 { SeenStatus } from '../../MessageSeenStatus';
import { loadCallsHistory } from '../../services/callHistoryLoader'; import { loadCallsHistory } from '../../services/callHistoryLoader';
import { setupBasics, asymmetricRoundtripHarness } from './helpers'; 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 { MessageAttributesType } from '../../model-types';
import type { AttachmentType } from '../../types/Attachment'; import { isVoiceMessage, type AttachmentType } from '../../types/Attachment';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { SignalService } from '../../protobuf';
const CONTACT_A = generateAci(); const CONTACT_A = generateAci();
@ -129,6 +130,33 @@ describe('backup/attachments', () => {
BackupLevel.Media 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', () => { describe('Preview attachments', () => {