Drop empty story replies and misattributed 1:1 messages

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-03-17 14:20:36 -05:00
committed by GitHub
parent 17d746bf40
commit edb1a01223
2 changed files with 85 additions and 8 deletions

View File

@@ -1535,6 +1535,20 @@ export class BackupExportStream extends Readable {
}
}
// Drop messages in the wrong 1:1 chat
if (conversation && isDirectConversation(conversation.attributes)) {
const convoAuthor = this.#getOrPushPrivateRecipient({
id: conversation.attributes.id,
});
if (authorId !== me && authorId !== convoAuthor) {
log.warn(
`${message.sent_at}: Dropping direct message with mismatched author`
);
return undefined;
}
}
if (isOutgoing || isIncoming) {
strictAssert(authorId, 'Incoming/outgoing messages require an author');
}
@@ -1791,10 +1805,15 @@ export class BackupExportStream extends Readable {
};
}
} else if (message.storyReplyContext || message.storyReaction) {
const directStoryReplyMessage = await this.#toDirectStoryReplyMessage({
message,
});
if (!directStoryReplyMessage) {
return undefined;
}
item = {
directStoryReplyMessage: await this.#toDirectStoryReplyMessage({
message,
}),
directStoryReplyMessage,
};
revisions = await this.#toChatItemRevisions(base, item, message);
@@ -3410,14 +3429,17 @@ export class BackupExportStream extends Readable {
| 'received_at'
| 'reactions'
>;
}): Promise<Backups.DirectStoryReplyMessage.Params> {
}): Promise<Backups.DirectStoryReplyMessage.Params | undefined> {
const reactions = this.#getMessageReactions(message);
let reply: Backups.DirectStoryReplyMessage.Params['reply'];
if (message.storyReaction) {
reply = { emoji: message.storyReaction.emoji };
} else {
} else if (message.body) {
reply = { textReply: await this.#toTextAndLongTextFields(message) };
} else {
log.warn('Dropping direct story reply message without reaction or body');
return undefined;
}
return { reply, reactions };
@@ -3532,10 +3554,15 @@ export class BackupExportStream extends Readable {
let item: Backups.ChatItem.Params['item'];
if (parentItem.directStoryReplyMessage) {
item = {
directStoryReplyMessage: await this.#toDirectStoryReplyMessage({
const directStoryReplyMessage =
await this.#toDirectStoryReplyMessage({
message: history,
}),
});
if (!directStoryReplyMessage) {
return undefined;
}
item = {
directStoryReplyMessage,
};
} else {
const standardMessage = await this.#toStandardMessage({

View File

@@ -354,6 +354,28 @@ describe('backup/bubble messages', () => {
);
});
it('drops misattributed incoming 1:1 messages', async () => {
await asymmetricRoundtripHarness(
[
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: CONTACT_B,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 3,
body: 'hello',
},
],
[]
);
});
it('updates incoming messages authored by self to outgoing', async () => {
const ourConversation = window.ConversationController.get(OUR_ACI);
strictAssert(ourConversation, 'our conversation exists');
@@ -1263,6 +1285,34 @@ describe('backup/bubble messages', () => {
await symmetricRoundtripHarness([incomingReply, outgoingReply]);
});
it('drops direct story text replies with no body', async () => {
strictAssert(ourConversation, 'conversation exists');
await asymmetricRoundtripHarness(
[
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
body: '',
unidentifiedDeliveryReceived: true,
sourceServiceId: CONTACT_A,
received_at: 3,
received_at_ms: 3,
sent_at: 3,
timestamp: 3,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
storyReplyContext: {
authorAci: OUR_ACI,
messageId: '',
},
},
],
[]
);
});
it('does not export group story replies', async () => {
strictAssert(ourConversation, 'conversations exist');
strictAssert(group, 'conversations exist');