Process out of order edits

This commit is contained in:
Josh Perez 2023-08-08 12:26:22 -04:00 committed by GitHub
parent b2cb722c01
commit 73737987fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 26 deletions

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { drop } from '../util/drop';
@ -23,12 +22,22 @@ export type EditAttributesType = {
const edits = new Map<string, EditAttributesType>();
export function forMessage(message: MessageModel): Array<EditAttributesType> {
const sentAt = getMessageSentTimestamp(message.attributes, { log });
export function forMessage(
messageAttributes: Pick<
MessageAttributesType,
| 'editMessageTimestamp'
| 'sent_at'
| 'source'
| 'sourceUuid'
| 'timestamp'
| 'type'
>
): Array<EditAttributesType> {
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
const matchingEdits = filter(edits, ([_envelopeId, item]) => {
return (
item.targetSentTimestamp === sentAt &&
item.fromId === getContactId(message.attributes)
item.fromId === getContactId(messageAttributes)
);
});
@ -44,7 +53,7 @@ export function forMessage(message: MessageModel): Array<EditAttributesType> {
});
log.info(
`Edits.forMessage(${message.get('sent_at')}): ` +
`Edits.forMessage(${messageAttributes.sent_at}): ` +
`Found early edits for message ${editsLogIds.join(', ')}`
);
return result;
@ -68,8 +77,7 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
);
if (!targetConversation) {
log.info(`${logId}: No target conversation`);
log.info(`${logId}: No message found`);
return;
}

View File

@ -137,7 +137,7 @@ export function isQuoteAMatch(
}
export function getContactId(
message: MessageAttributesType
message: Pick<MessageAttributesType, 'type' | 'source' | 'sourceUuid'>
): string | undefined {
const source = getSource(message);
const sourceUuid = getSourceUuid(message);

View File

@ -27,25 +27,27 @@ function wrap({
};
}
function createMessage(): Proto.IDataMessage {
function createMessage(body: string): Proto.IDataMessage {
return {
body: 'hey yhere',
body,
groupV2: undefined,
timestamp: Long.fromNumber(Date.now()),
};
}
function createEditedMessage(
targetMessage: Proto.IDataMessage
targetSentTimestamp: Long | null | undefined,
body: string,
timestamp = Date.now()
): Proto.IEditMessage {
strictAssert(targetMessage.timestamp, 'timestamp missing');
strictAssert(targetSentTimestamp, 'timestamp missing');
return {
targetSentTimestamp: targetMessage.timestamp,
targetSentTimestamp,
dataMessage: {
body: 'hey there',
body,
groupV2: undefined,
timestamp: Long.fromNumber(Date.now()),
timestamp: Long.fromNumber(timestamp),
},
};
}
@ -77,7 +79,8 @@ describe('editing', function needsName() {
const window = await app.getWindow();
const originalMessage = createMessage();
const initialMessageBody = 'hey yhere';
const originalMessage = createMessage(initialMessageBody);
const originalMessageTimestamp = Number(originalMessage.timestamp);
debug('sending message');
@ -101,7 +104,9 @@ describe('editing', function needsName() {
await window.locator('.module-conversation-hero').waitFor();
debug('checking for message');
await window.locator('.module-message__text >> "hey yhere"').waitFor();
await window
.locator(`.module-message__text >> "${initialMessageBody}"`)
.waitFor();
debug('waiting for receipts for original message');
const receipts = await app.waitForReceipts();
@ -110,7 +115,11 @@ describe('editing', function needsName() {
assert.strictEqual(receipts.timestamps[0], originalMessageTimestamp);
debug('sending edited message');
const editedMessage = createEditedMessage(originalMessage);
const editedMessageBody = 'hey there';
const editedMessage = createEditedMessage(
originalMessage.timestamp,
editedMessageBody
);
const editedMessageTimestamp = Number(editedMessage.dataMessage?.timestamp);
{
const sendOptions = {
@ -124,7 +133,9 @@ describe('editing', function needsName() {
}
debug('checking for edited message');
await window.locator('.module-message__text >> "hey there"').waitFor();
await window
.locator(`.module-message__text >> "${editedMessageBody}"`)
.waitFor();
const messages = window.locator('.module-message__text');
assert.strictEqual(await messages.count(), 1, 'message count');
@ -137,7 +148,8 @@ describe('editing', function needsName() {
const [friend] = contacts;
const originalMessage = createMessage();
const initialMessageBody = 'hey yhere';
const originalMessage = createMessage(initialMessageBody);
const originalMessageTimestamp = Number(originalMessage.timestamp);
debug('incoming message');
@ -161,7 +173,9 @@ describe('editing', function needsName() {
await window.locator('.module-conversation-hero').waitFor();
debug('checking for message');
await window.locator('.module-message__text >> "hey yhere"').waitFor();
await window
.locator(`.module-message__text >> "${initialMessageBody}"`)
.waitFor();
debug('waiting for original receipt');
const originalReceipt = await friend.waitForReceipt();
@ -177,7 +191,11 @@ describe('editing', function needsName() {
}
debug('sending edited message');
const editedMessage = createEditedMessage(originalMessage);
const editedMessageBody = 'hey there';
const editedMessage = createEditedMessage(
originalMessage.timestamp,
editedMessageBody
);
const editedMessageTimestamp = Number(editedMessage.dataMessage?.timestamp);
{
const sendOptions = {
@ -191,7 +209,9 @@ describe('editing', function needsName() {
}
debug('checking for edited message');
await window.locator('.module-message__text >> "hey there"').waitFor();
await window
.locator(`.module-message__text >> "${editedMessageBody}"`)
.waitFor();
const messages = window.locator('.module-message__text');
assert.strictEqual(await messages.count(), 1, 'message count');
@ -264,6 +284,7 @@ describe('editing', function needsName() {
await input.press('Enter');
}
debug("waiting for friend's edit message");
const { editMessage: firstEdit } = await friend.waitForEditMessage();
assert.strictEqual(
firstEdit.targetSentTimestamp?.toNumber(),
@ -311,4 +332,75 @@ describe('editing', function needsName() {
assert.isTrue(await history.locator('"edit message 2"').isVisible());
assert.isTrue(await history.locator('"edit message 3"').isVisible());
});
it('is fine with out of order edit processing', async () => {
const { phone, desktop } = bootstrap;
const window = await app.getWindow();
const originalMessage = createMessage('v1');
const originalMessageTimestamp = Number(originalMessage.timestamp);
const sendOriginalMessage = async () => {
debug('sending original message', originalMessageTimestamp);
const sendOptions = {
timestamp: originalMessageTimestamp,
};
await phone.sendRaw(
desktop,
wrap({ dataMessage: originalMessage }),
sendOptions
);
};
debug('sending all messages + edits');
let targetSentTimestamp = originalMessage.timestamp;
let editTimestamp = Date.now() + 1;
const editedMessages: Array<Proto.IEditMessage> = [
'v2',
'v3',
'v4',
'v5',
].map(body => {
const message = createEditedMessage(
targetSentTimestamp,
body,
editTimestamp
);
targetSentTimestamp = Long.fromNumber(editTimestamp);
editTimestamp += 1;
return message;
});
{
const sendEditMessages = editedMessages.map(editMessage => {
const timestamp = Number(editMessage.dataMessage?.timestamp);
const sendOptions = {
timestamp,
};
return () => {
debug(
`sending edit timestamp=${timestamp}, target=${editMessage.targetSentTimestamp}`
);
return phone.sendRaw(desktop, wrap({ editMessage }), sendOptions);
};
});
await Promise.all(sendEditMessages.reverse().map(f => f()));
await sendOriginalMessage();
}
debug('opening conversation');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator('.module-conversation-list__item--contact-or-conversation')
.first()
.click();
await window.locator('.module-conversation-hero').waitFor();
debug('checking for latest message');
await window.locator('.module-message__text >> "v5"').waitFor();
const messages = window.locator('.module-message__text');
assert.strictEqual(await messages.count(), 1, 'message count');
});
});

View File

@ -9,6 +9,7 @@ import type {
QuotedMessageType,
} from '../model-types.d';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as Edits from '../messageModifiers/Edits';
import * as durations from './durations';
import * as log from '../logging/log';
import { ReadStatus } from '../messages/MessageReadStatus';
@ -23,14 +24,26 @@ import { isDirectConversation } from './whatTypeOfConversation';
import { queueAttachmentDownloads } from './queueAttachmentDownloads';
import { modifyTargetMessage } from './modifyTargetMessage';
const RECURSION_LIMIT = 15;
export async function handleEditMessage(
mainMessage: MessageAttributesType,
editAttributes: Pick<
EditAttributesType,
'message' | 'conversationId' | 'fromDevice' | 'fromId'
>
>,
recursionCount = 0
): Promise<void> {
const idLog = `handleEditMessage(${getMessageIdForLogging(mainMessage)})`;
const idLog = `handleEditMessage(edit=${
editAttributes.message.timestamp
},original=${getMessageIdForLogging(mainMessage)})`;
if (recursionCount >= RECURSION_LIMIT) {
log.warn(`${idLog}: Too much recursion`);
return;
}
log.info(idLog);
// Verify that we can safely apply an edit to this type of message
if (mainMessage.deletedForEveryone) {
@ -298,9 +311,23 @@ export async function handleEditMessage(
const mainMessageConversation = mainMessageModel.getConversation();
if (mainMessageConversation) {
drop(mainMessageConversation.updateLastMessage());
// Apply any other operations, excluding edits that target this message
await modifyTargetMessage(mainMessageModel, mainMessageConversation, {
isFirstRun: true,
skipEdits: true,
});
}
// Apply any other pending edits that target this message
const edits = Edits.forMessage({
...mainMessage,
sent_at: editedMessage.timestamp,
timestamp: editedMessage.timestamp,
});
log.info(`${idLog}: ${edits.length} edits`);
await Promise.all(
edits.map(edit =>
handleEditMessage(mainMessageModel.attributes, edit, recursionCount + 1)
)
);
}

View File

@ -252,7 +252,7 @@ export async function modifyTargetMessage(
// We want to make sure the message is saved first before applying any edits
if (!isFirstRun && !skipEdits) {
const edits = Edits.forMessage(message);
const edits = Edits.forMessage(message.attributes);
log.info(`${logId}: ${edits.length} edits in second run`);
await Promise.all(
edits.map(editAttributes =>