Fix roundtripping of voice notes with body

This commit is contained in:
Fedor Indutny 2025-04-16 14:27:47 -07:00 committed by GitHub
commit 0b5f0df1ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 14 deletions

View file

@ -38,7 +38,6 @@ import type {
ConversationAttributesType, ConversationAttributesType,
MessageAttributesType, MessageAttributesType,
QuotedAttachmentType, QuotedAttachmentType,
QuotedMessageType,
} from '../../model-types.d'; } from '../../model-types.d';
import { drop } from '../../util/drop'; import { drop } from '../../util/drop';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
@ -2195,14 +2194,13 @@ export class BackupExportStream extends Readable {
} }
async #toQuote({ async #toQuote({
quote, message,
backupLevel, backupLevel,
messageReceivedAt,
}: { }: {
quote?: QuotedMessageType; message: Pick<MessageAttributesType, 'quote' | 'received_at' | 'body'>;
backupLevel: BackupLevel; backupLevel: BackupLevel;
messageReceivedAt: number;
}): Promise<Backups.IQuote | null> { }): Promise<Backups.IQuote | null> {
const { quote } = message;
if (!quote) { if (!quote) {
return null; return null;
} }
@ -2259,7 +2257,7 @@ export class BackupExportStream extends Readable {
? await this.#processMessageAttachment({ ? await this.#processMessageAttachment({
attachment: attachment.thumbnail, attachment: attachment.thumbnail,
backupLevel, backupLevel,
messageReceivedAt, message,
}) })
: undefined, : undefined,
}; };
@ -2287,11 +2285,17 @@ export class BackupExportStream extends Readable {
} }
#getMessageAttachmentFlag( #getMessageAttachmentFlag(
message: Pick<MessageAttributesType, 'body'>,
attachment: AttachmentType attachment: AttachmentType
): Backups.MessageAttachment.Flag { ): Backups.MessageAttachment.Flag {
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (((attachment.flags || 0) & flag) === flag) { if (((attachment.flags || 0) & flag) === flag) {
// Legacy data support for iOS
if (message.body) {
return Backups.MessageAttachment.Flag.NONE;
}
return Backups.MessageAttachment.Flag.VOICE_MESSAGE; return Backups.MessageAttachment.Flag.VOICE_MESSAGE;
} }
if (isGIF([attachment])) { if (isGIF([attachment])) {
@ -2311,22 +2315,22 @@ export class BackupExportStream extends Readable {
async #processMessageAttachment({ async #processMessageAttachment({
attachment, attachment,
backupLevel, backupLevel,
messageReceivedAt, message,
}: { }: {
attachment: AttachmentType; attachment: AttachmentType;
backupLevel: BackupLevel; backupLevel: BackupLevel;
messageReceivedAt: number; message: Pick<MessageAttributesType, 'quote' | 'received_at' | 'body'>;
}): Promise<Backups.MessageAttachment> { }): Promise<Backups.MessageAttachment> {
const { clientUuid } = attachment; const { clientUuid } = attachment;
const filePointer = await this.#processAttachment({ const filePointer = await this.#processAttachment({
attachment, attachment,
backupLevel, backupLevel,
messageReceivedAt, messageReceivedAt: message.received_at,
}); });
return new Backups.MessageAttachment({ return new Backups.MessageAttachment({
pointer: filePointer, pointer: filePointer,
flag: this.#getMessageAttachmentFlag(attachment), flag: this.#getMessageAttachmentFlag(message, attachment),
wasDownloaded: isDownloaded(attachment), wasDownloaded: isDownloaded(attachment),
clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined, clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined,
}); });
@ -2550,9 +2554,8 @@ export class BackupExportStream extends Readable {
}): Promise<Backups.IStandardMessage> { }): Promise<Backups.IStandardMessage> {
return { return {
quote: await this.#toQuote({ quote: await this.#toQuote({
quote: message.quote, message,
backupLevel, backupLevel,
messageReceivedAt: message.received_at,
}), }),
attachments: message.attachments?.length attachments: message.attachments?.length
? await Promise.all( ? await Promise.all(
@ -2560,7 +2563,7 @@ export class BackupExportStream extends Readable {
return this.#processMessageAttachment({ return this.#processMessageAttachment({
attachment, attachment,
backupLevel, backupLevel,
messageReceivedAt: message.received_at, message,
}); });
}) })
) )
@ -2667,7 +2670,7 @@ export class BackupExportStream extends Readable {
: await this.#processMessageAttachment({ : await this.#processMessageAttachment({
attachment, attachment,
backupLevel, backupLevel,
messageReceivedAt: message.received_at, message,
}), }),
reactions: this.#getMessageReactions(message), reactions: this.#getMessageReactions(message),
}; };

View file

@ -355,6 +355,39 @@ describe('backup/attachments', () => {
{ backupLevel: BackupLevel.Paid } { backupLevel: BackupLevel.Paid }
); );
}); });
it('drops voice message flag when body is present', 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, {
body: 'hello',
attachments: [attachment],
}),
],
[
composeMessage(1, {
body: 'hello',
hasAttachments: true,
attachments: [
{
...omit(attachment, NON_ROUNDTRIPPED_BACKUP_LOCATOR_FIELDS),
flags: undefined,
backupLocator: {
mediaName: digestToMediaName(attachment.digest),
},
},
],
}),
],
{ backupLevel: BackupLevel.Paid }
);
});
}); });
describe('Preview attachments', () => { describe('Preview attachments', () => {