diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index c142bfc1cc..b1037aaac8 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -210,6 +210,7 @@ export class BackupExportStream extends Readable { private readonly serviceIdToRecipientId = new Map(); private readonly e164ToRecipientId = new Map(); private readonly roomIdToRecipientId = new Map(); + private ourConversation?: ConversationAttributesType; private attachmentBackupJobs: Array = []; private buffers = new Array(); private nextRecipientId = 1; @@ -256,6 +257,8 @@ export class BackupExportStream extends Readable { } private async unsafeRun(backupLevel: BackupLevel): Promise { + this.ourConversation = + window.ConversationController.getOurConversationOrThrow().attributes; this.push( Backups.BackupInfo.encodeDelimited({ version: Long.fromNumber(BACKUP_VERSION), @@ -1073,7 +1076,8 @@ export class BackupExportStream extends Readable { if (authorId === me) { result.outgoing = this.getOutgoingMessageDetails( message.sent_at, - message + message, + { conversationId: message.conversationId } ); } else { result.incoming = this.getIncomingMessageDetails(message); @@ -1229,7 +1233,8 @@ export class BackupExportStream extends Readable { if (isOutgoing) { result.outgoing = this.getOutgoingMessageDetails( message.sent_at, - message + message, + { conversationId: message.conversationId } ); } else { result.incoming = this.getIncomingMessageDetails(message); @@ -2345,7 +2350,8 @@ export class BackupExportStream extends Readable { }: Pick< MessageAttributesType, 'sendStateByConversationId' | 'unidentifiedDeliveries' | 'errors' - > + >, + { conversationId }: { conversationId: string } ): Backups.ChatItem.IOutgoingMessageDetails { const sealedSenderServiceIds = new Set(unidentifiedDeliveries); const errorMap = new Map( @@ -2361,6 +2367,17 @@ export class BackupExportStream extends Readable { log.warn(`backups: no send target for a message ${sentAt}`); continue; } + + // Filter out our conversationId from non-"Note-to-Self" messages + // TODO: DESKTOP-8089 + strictAssert(this.ourConversation?.id, 'our conversation must exist'); + if ( + id === this.ourConversation.id && + conversationId !== this.ourConversation.id + ) { + continue; + } + const { serviceId } = target.attributes; const recipientId = this.getOrPushPrivateRecipient(target.attributes); const timestamp = @@ -2561,7 +2578,9 @@ export class BackupExportStream extends Readable { // Directional details outgoing: isOutgoing - ? this.getOutgoingMessageDetails(history.timestamp, history) + ? this.getOutgoingMessageDetails(history.timestamp, history, { + conversationId: message.conversationId, + }) : undefined, incoming: isOutgoing ? undefined diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 597e57df00..2b51dcf92d 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -1499,7 +1499,23 @@ export class BackupImportStream extends Writable { const unidentifiedDeliveries = new Array(); const errors = new Array(); - for (const status of outgoing.sendStatus ?? []) { + + let sendStatuses = outgoing.sendStatus; + if (!sendStatuses?.length) { + // TODO: DESKTOP-8089 + // If this outgoing message was not sent to anyone, we add ourselves to + // sendStateByConversationId and mark read. This is to match existing desktop + // behavior. + sendStatuses = [ + { + recipientId: item.authorId, + read: new Backups.SendStatus.Read(), + timestamp: item.dateSent, + }, + ]; + } + + for (const status of sendStatuses) { strictAssert( status.recipientId, 'sendStatus recipient must have an id' diff --git a/ts/test-electron/backup/bubble_test.ts b/ts/test-electron/backup/bubble_test.ts index 7d0720b559..86de39c534 100644 --- a/ts/test-electron/backup/bubble_test.ts +++ b/ts/test-electron/backup/bubble_test.ts @@ -22,6 +22,8 @@ import { OUR_ACI, } from './helpers'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders'; +import { strictAssert } from '../../util/assert'; +import type { MessageAttributesType } from '../../model-types'; const CONTACT_A = generateAci(); const CONTACT_B = generateAci(); @@ -603,4 +605,155 @@ describe('backup/bubble messages', () => { }, ]); }); + describe('lonely-in-group messages', async () => { + const GROUP_ID = Bytes.toBase64(getRandomBytes(32)); + let group: ConversationModel | undefined; + let ourConversation: ConversationModel | undefined; + + beforeEach(async () => { + group = await window.ConversationController.getOrCreateAndWait( + GROUP_ID, + 'group', + { + groupVersion: 2, + masterKey: Bytes.toBase64(getRandomBytes(32)), + name: 'Rock Enthusiasts', + active_at: 1, + } + ); + ourConversation = window.ConversationController.get(OUR_ACI); + }); + + it('roundtrips messages that have our id in sendStateByConversationId', async () => { + strictAssert(group, 'conversations exist'); + strictAssert(ourConversation, 'conversations exist'); + await symmetricRoundtripHarness([ + { + conversationId: group.id, + id: generateGuid(), + type: 'outgoing', + received_at: 3, + received_at_ms: 3, + sent_at: 3, + timestamp: 3, + sourceServiceId: OUR_ACI, + body: 'd', + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + sendStateByConversationId: { + [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 }, + }, + expirationStartTimestamp: Date.now(), + expireTimer: DurationInSeconds.fromMillis(WEEK), + }, + ]); + }); + it( + 'if a message did not have sendStateByConversationId (e.g. to mimic post-import from primary), ' + + 'would add it with our conversationId when importing', + async () => { + strictAssert(group, 'conversations exist'); + strictAssert(ourConversation, 'conversations exist'); + const message: MessageAttributesType = { + conversationId: group.id, + id: generateGuid(), + type: 'outgoing', + received_at: 3, + received_at_ms: 3, + sent_at: 3, + timestamp: 3, + sourceServiceId: OUR_ACI, + body: 'd', + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + expirationStartTimestamp: Date.now(), + expireTimer: DurationInSeconds.fromMillis(WEEK), + }; + + await asymmetricRoundtripHarness( + [ + { + ...message, + sendStateByConversationId: {}, + }, + ], + [ + { + ...message, + sendStateByConversationId: { + [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 }, + }, + }, + ] + ); + } + ); + it('filters out our conversation id from sendStateByConversationId in non-note-to-self convos', async () => { + strictAssert(group, 'conversations exist'); + strictAssert(ourConversation, 'conversations exist'); + const message: MessageAttributesType = { + conversationId: group.id, + id: generateGuid(), + type: 'outgoing', + received_at: 3, + received_at_ms: 3, + sent_at: 3, + timestamp: 3, + sourceServiceId: OUR_ACI, + body: 'd', + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + expirationStartTimestamp: Date.now(), + expireTimer: DurationInSeconds.fromMillis(WEEK), + }; + + await asymmetricRoundtripHarness( + [ + { + ...message, + sendStateByConversationId: { + [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 }, + [contactA.id]: { status: SendStatus.Delivered, updatedAt: 4 }, + }, + }, + ], + [ + { + ...message, + sendStateByConversationId: { + [contactA.id]: { status: SendStatus.Delivered, updatedAt: 4 }, + }, + }, + ] + ); + }); + it('does not filter out our conversation id from sendStateByConversationId in Note-to-Self', async () => { + strictAssert(ourConversation, 'conversations exist'); + const message: MessageAttributesType = { + conversationId: ourConversation.id, + id: generateGuid(), + type: 'outgoing', + received_at: 3, + received_at_ms: 3, + sent_at: 3, + timestamp: 3, + sourceServiceId: OUR_ACI, + body: 'd', + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + expirationStartTimestamp: Date.now(), + expireTimer: DurationInSeconds.fromMillis(WEEK), + }; + ourConversation.set({ active_at: 3 }); + + await symmetricRoundtripHarness([ + { + ...message, + sendStateByConversationId: { + [ourConversation.id]: { status: SendStatus.Read, updatedAt: 3 }, + }, + }, + ]); + }); + }); });