Update sendStateByConversationId for lonely groups

This commit is contained in:
trevor-signal 2025-01-09 14:56:06 -05:00 committed by GitHub
parent d24da2e26d
commit 1e7d259909
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 193 additions and 5 deletions

View file

@ -210,6 +210,7 @@ export class BackupExportStream extends Readable {
private readonly serviceIdToRecipientId = new Map<string, number>();
private readonly e164ToRecipientId = new Map<string, number>();
private readonly roomIdToRecipientId = new Map<string, number>();
private ourConversation?: ConversationAttributesType;
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
private buffers = new Array<Uint8Array>();
private nextRecipientId = 1;
@ -256,6 +257,8 @@ export class BackupExportStream extends Readable {
}
private async unsafeRun(backupLevel: BackupLevel): Promise<void> {
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

View file

@ -1499,7 +1499,23 @@ export class BackupImportStream extends Writable {
const unidentifiedDeliveries = new Array<ServiceIdString>();
const errors = new Array<CustomError>();
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'

View file

@ -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 },
},
},
]);
});
});
});