signal-desktop/ts/messageModifiers/Edits.ts
2024-07-22 11:16:33 -07:00

152 lines
4 KiB
TypeScript

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { DataReader } from '../sql/Client';
import { drop } from '../util/drop';
import { getAuthorId } from '../messages/helpers';
import { handleEditMessage } from '../util/handleEditMessage';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import {
isAttachmentDownloadQueueEmpty,
registerQueueEmptyCallback,
} from '../util/attachmentDownloadQueue';
export type EditAttributesType = {
conversationId: string;
envelopeId: string;
fromId: string;
fromDevice: number;
message: MessageAttributesType;
targetSentTimestamp: number;
removeFromMessageReceiverCache: () => unknown;
};
const edits = new Map<string, EditAttributesType>();
function remove(edit: EditAttributesType): void {
edits.delete(edit.envelopeId);
edit.removeFromMessageReceiverCache();
}
export function forMessage(
messageAttributes: Pick<
MessageAttributesType,
| 'editMessageTimestamp'
| 'sent_at'
| 'source'
| 'sourceServiceId'
| 'timestamp'
| 'type'
>
): Array<EditAttributesType> {
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
const editValues = Array.from(edits.values());
if (!isAttachmentDownloadQueueEmpty()) {
log.info(
'Edits.forMessage attachmentDownloadQueue not empty, not processing edits'
);
registerQueueEmptyCallback(flushEdits);
return [];
}
const matchingEdits = editValues.filter(item => {
return (
item.targetSentTimestamp === sentAt &&
item.fromId === getAuthorId(messageAttributes)
);
});
if (matchingEdits.length > 0) {
const editsLogIds: Array<number> = [];
const result = matchingEdits.map(item => {
editsLogIds.push(item.message.sent_at);
remove(item);
return item;
});
log.info(
`Edits.forMessage(${messageAttributes.sent_at}): ` +
`Found early edits for message ${editsLogIds.join(', ')}`
);
return result;
}
return [];
}
export async function flushEdits(): Promise<void> {
log.info('Edits.flushEdits running');
return drop(
Promise.all(Array.from(edits.values()).map(edit => onEdit(edit)))
);
}
export async function onEdit(edit: EditAttributesType): Promise<void> {
edits.set(edit.envelopeId, edit);
const logId = `Edits.onEdit(timestamp=${edit.message.timestamp};target=${edit.targetSentTimestamp})`;
if (!isAttachmentDownloadQueueEmpty()) {
log.info(
`${logId}: attachmentDownloadQueue not empty, not processing edits`
);
registerQueueEmptyCallback(flushEdits);
return;
}
try {
// The conversation the edited message was in; we have to find it in the database
// to to figure that out.
const targetConversation =
await window.ConversationController.getConversationForTargetMessage(
edit.fromId,
edit.targetSentTimestamp
);
if (!targetConversation) {
log.info(`${logId}: No message found`);
return;
}
// Do not await, since this can deadlock the queue
drop(
targetConversation.queueJob('Edits.onEdit', async () => {
log.info(`${logId}: Handling edit`);
const messages = await DataReader.getMessagesBySentAt(
edit.targetSentTimestamp
);
// Verify authorship
const targetMessage = messages.find(
m =>
edit.conversationId === m.conversationId &&
edit.fromId === getAuthorId(m)
);
if (!targetMessage) {
log.info(`${logId}: No message`);
return;
}
const message = window.MessageCache.__DEPRECATED$register(
targetMessage.id,
targetMessage,
'Edits.onEdit'
);
await handleEditMessage(message.attributes, edit);
remove(edit);
})
);
} catch (error) {
remove(edit);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}