Support voice memo backups
This commit is contained in:
parent
348503c7f9
commit
9cbbbe0ef0
4 changed files with 82 additions and 19 deletions
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue