Move receipts and view/read syncs to new syncTasks system

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal 2024-06-17 19:36:57 -05:00 committed by GitHub
parent 949104c316
commit b95dd1a70f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1242 additions and 612 deletions

View file

@ -42,7 +42,10 @@ import { isNotNil } from './util/isNotNil';
import { isBackupEnabled } from './util/isBackupEnabled';
import { setAppLoadingScreenMessage } from './setAppLoadingScreenMessage';
import { IdleDetector } from './IdleDetector';
import { expiringMessagesDeletionService } from './services/expiringMessagesDeletion';
import {
initialize as initializeExpiringMessageService,
update as updateExpiringMessagesService,
} from './services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from './services/tapToViewMessagesDeletionService';
import { getStoriesForRedux, loadStories } from './services/storyLoader';
import {
@ -116,17 +119,12 @@ import * as Edits from './messageModifiers/Edits';
import * as MessageReceipts from './messageModifiers/MessageReceipts';
import * as MessageRequests from './messageModifiers/MessageRequests';
import * as Reactions from './messageModifiers/Reactions';
import * as ReadSyncs from './messageModifiers/ReadSyncs';
import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
import * as ViewSyncs from './messageModifiers/ViewSyncs';
import type { DeleteAttributesType } from './messageModifiers/Deletes';
import type { EditAttributesType } from './messageModifiers/Edits';
import type { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
import type { ReactionAttributesType } from './messageModifiers/Reactions';
import type { ReadSyncAttributesType } from './messageModifiers/ReadSyncs';
import type { ViewOnceOpenSyncAttributesType } from './messageModifiers/ViewOnceOpenSyncs';
import type { ViewSyncAttributesType } from './messageModifiers/ViewSyncs';
import { ReadStatus } from './messages/MessageReadStatus';
import type { SendStateByConversationId } from './messages/MessageSendState';
import { SendStatus } from './messages/MessageSendState';
@ -202,7 +200,11 @@ import { getThemeType } from './util/getThemeType';
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
import { CallMode } from './types/Calling';
import type { SyncTaskType } from './util/syncTasks';
import { queueSyncTasks } from './util/syncTasks';
import type { ViewSyncTaskType } from './messageModifiers/ViewSyncs';
import type { ReceiptSyncTaskType } from './messageModifiers/MessageReceipts';
import type { ReadSyncTaskType } from './messageModifiers/ReadSyncs';
import { isEnabled } from './RemoteConfig';
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
import { getConversationIdForLogging } from './util/idForLogging';
@ -1498,10 +1500,12 @@ export async function startApp(): Promise<void> {
window.Whisper.events.trigger('timetravel');
});
void expiringMessagesDeletionService.update();
initializeExpiringMessageService(singleProtoJobQueue);
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
window.Whisper.events.on('timetravel', () => {
void expiringMessagesDeletionService.update();
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
});
@ -1833,7 +1837,9 @@ export async function startApp(): Promise<void> {
try {
// Note: we always have to register our capabilities all at once, so we do this
// after connect on every startup
await server.registerCapabilities({});
await server.registerCapabilities({
deleteSync: true,
});
} catch (error) {
log.error(
'Error: Unable to register our capabilities.',
@ -3221,200 +3227,329 @@ export async function startApp(): Promise<void> {
drop(MessageRequests.onResponse(attributes));
}
function onReadReceipt(event: Readonly<ReadEvent>): void {
onReadOrViewReceipt({
async function onReadReceipt(event: Readonly<ReadEvent>): Promise<void> {
return onReadOrViewReceipt({
logTitle: 'read receipt',
event,
type: MessageReceipts.MessageReceiptType.Read,
type: MessageReceipts.messageReceiptTypeSchema.enum.Read,
});
}
function onViewReceipt(event: Readonly<ViewEvent>): void {
onReadOrViewReceipt({
async function onViewReceipt(event: Readonly<ViewEvent>): Promise<void> {
return onReadOrViewReceipt({
logTitle: 'view receipt',
event,
type: MessageReceipts.MessageReceiptType.View,
type: MessageReceipts.messageReceiptTypeSchema.enum.View,
});
}
function onReadOrViewReceipt({
async function onReadOrViewReceipt({
event,
logTitle,
type,
}: Readonly<{
event: ReadEvent | ViewEvent;
logTitle: string;
type:
| MessageReceipts.MessageReceiptType.Read
| MessageReceipts.MessageReceiptType.View;
}>): void {
const {
envelopeTimestamp,
timestamp,
source,
sourceServiceId,
sourceDevice,
wasSentEncrypted,
} = event.receipt;
const sourceConversation = window.ConversationController.lookupOrCreate({
serviceId: sourceServiceId,
e164: source,
reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
});
strictAssert(sourceConversation, 'Failed to create conversation');
log.info(
logTitle,
`${sourceServiceId || source}.${sourceDevice}`,
envelopeTimestamp,
'for sent message',
timestamp
);
type: 'Read' | 'View';
}>): Promise<void> {
const { receipts, envelopeId, envelopeTimestamp, confirm } = event;
const logId = `onReadOrViewReceipt(type=${type}, envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
strictAssert(
isServiceIdString(sourceServiceId),
'onReadOrViewReceipt: Missing sourceServiceId'
);
strictAssert(sourceDevice, 'onReadOrViewReceipt: Missing sourceDevice');
const syncTasks = receipts
.map((receipt): SyncTaskType | undefined => {
const {
timestamp,
source,
sourceServiceId,
sourceDevice,
wasSentEncrypted,
} = receipt;
const sourceConversation = window.ConversationController.lookupOrCreate(
{
serviceId: sourceServiceId,
e164: source,
reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
}
);
const attributes: MessageReceiptAttributesType = {
envelopeId: event.receipt.envelopeId,
removeFromMessageReceiverCache: event.confirm,
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId: sourceConversation.id,
sourceServiceId,
sourceDevice,
type,
wasSentEncrypted,
};
drop(MessageReceipts.onReceipt(attributes));
log.info(
logTitle,
`${sourceServiceId || source}.${sourceDevice}`,
envelopeTimestamp,
'for sent message',
timestamp
);
if (!sourceConversation) {
log.error(`${logId}: Failed to create conversation`);
return undefined;
}
if (!isServiceIdString(sourceServiceId)) {
log.error(`${logId}: Missing sourceServiceId`);
return undefined;
}
if (!sourceDevice) {
log.error(`${logId}: Missing sourceDevice`);
return undefined;
}
const data: ReceiptSyncTaskType = {
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId: sourceConversation.id,
sourceServiceId,
sourceDevice,
type,
wasSentEncrypted,
};
return {
id: generateUuid(),
attempts: 1,
createdAt: Date.now(),
data,
envelopeId,
sentAt: envelopeTimestamp,
type,
};
})
.filter(isNotNil);
log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
await window.Signal.Data.saveSyncTasks(syncTasks);
confirm();
log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
log.info(`${logId}: Done`);
}
async function onReadSync(ev: ReadSyncEvent): Promise<void> {
const { envelopeTimestamp, sender, senderAci, timestamp } = ev.read;
const readAt = envelopeTimestamp;
const { conversation: senderConversation } =
window.ConversationController.maybeMergeContacts({
aci: senderAci,
e164: sender,
reason: 'onReadSync',
});
const senderId = senderConversation?.id;
const { reads, envelopeTimestamp, envelopeId, confirm } = ev;
const logId = `onReadSync(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
log.info(
'read sync',
sender,
senderAci,
envelopeTimestamp,
senderId,
'for message',
timestamp
);
const syncTasks = reads
.map((read): SyncTaskType | undefined => {
const { sender, senderAci, timestamp } = read;
const readAt = envelopeTimestamp;
const { conversation: senderConversation } =
window.ConversationController.maybeMergeContacts({
aci: senderAci,
e164: sender,
reason: 'onReadSync',
});
const senderId = senderConversation?.id;
strictAssert(senderId, 'onReadSync missing senderId');
strictAssert(senderAci, 'onReadSync missing senderAci');
strictAssert(timestamp, 'onReadSync missing timestamp');
log.info(
'read sync',
sender,
senderAci,
envelopeTimestamp,
senderId,
'for message',
timestamp
);
const attributes: ReadSyncAttributesType = {
envelopeId: ev.read.envelopeId,
removeFromMessageReceiverCache: ev.confirm,
senderId,
sender,
senderAci,
timestamp,
readAt,
};
if (!senderId) {
log.error(`${logId}: missing senderId`);
return undefined;
}
if (!senderAci) {
log.error(`${logId}: missing senderAci`);
return undefined;
}
if (!timestamp) {
log.error(`${logId}: missing timestamp`);
return undefined;
}
await ReadSyncs.onSync(attributes);
const data: ReadSyncTaskType = {
type: 'ReadSync',
senderId,
sender,
senderAci,
timestamp,
readAt,
};
return {
id: generateUuid(),
attempts: 1,
createdAt: Date.now(),
data,
envelopeId,
sentAt: envelopeTimestamp,
type: 'ReadSync',
};
})
.filter(isNotNil);
log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
await window.Signal.Data.saveSyncTasks(syncTasks);
confirm();
log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
log.info(`${logId}: Done`);
}
async function onViewSync(ev: ViewSyncEvent): Promise<void> {
const { envelopeTimestamp, senderE164, senderAci, timestamp } = ev.view;
const { conversation: senderConversation } =
window.ConversationController.maybeMergeContacts({
e164: senderE164,
aci: senderAci,
reason: 'onViewSync',
});
const senderId = senderConversation?.id;
const { envelopeTimestamp, envelopeId, views, confirm } = ev;
const logId = `onViewSync=(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
log.info(
'view sync',
senderE164,
senderAci,
envelopeTimestamp,
senderId,
'for message',
timestamp
);
const syncTasks = views
.map((view): SyncTaskType | undefined => {
const { senderAci, senderE164, timestamp } = view;
strictAssert(senderId, 'onViewSync missing senderId');
strictAssert(senderAci, 'onViewSync missing senderAci');
strictAssert(timestamp, 'onViewSync missing timestamp');
const { conversation: senderConversation } =
window.ConversationController.maybeMergeContacts({
e164: senderE164,
aci: senderAci,
reason: 'onViewSync',
});
const senderId = senderConversation?.id;
const attributes: ViewSyncAttributesType = {
envelopeId: ev.view.envelopeId,
removeFromMessageReceiverCache: ev.confirm,
senderId,
senderE164,
senderAci,
timestamp,
viewedAt: envelopeTimestamp,
};
log.info(
'view sync',
senderE164,
senderAci,
envelopeTimestamp,
senderId,
'for message',
timestamp
);
await ViewSyncs.onSync(attributes);
if (!senderId) {
log.error(`${logId}: missing senderId`);
return undefined;
}
if (!senderAci) {
log.error(`${logId}: missing senderAci`);
return undefined;
}
if (!timestamp) {
log.error(`${logId}: missing timestamp`);
return undefined;
}
const data: ViewSyncTaskType = {
type: 'ViewSync',
senderId,
senderE164,
senderAci,
timestamp,
viewedAt: envelopeTimestamp,
};
return {
id: generateUuid(),
attempts: 1,
createdAt: Date.now(),
data,
envelopeId,
sentAt: envelopeTimestamp,
type: 'ViewSync',
};
})
.filter(isNotNil);
log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
await window.Signal.Data.saveSyncTasks(syncTasks);
confirm();
log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
log.info(`${logId}: Done`);
}
function onDeliveryReceipt(ev: DeliveryEvent): void {
const { deliveryReceipt } = ev;
const {
envelopeTimestamp,
sourceServiceId,
source,
sourceDevice,
timestamp,
wasSentEncrypted,
} = deliveryReceipt;
async function onDeliveryReceipt(ev: DeliveryEvent): Promise<void> {
const { deliveryReceipts, envelopeId, envelopeTimestamp, confirm } = ev;
const logId = `onDeliveryReceipt(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
const sourceConversation = window.ConversationController.lookupOrCreate({
serviceId: sourceServiceId,
e164: source,
reason: `onDeliveryReceipt(${envelopeTimestamp})`,
});
strictAssert(envelopeTimestamp, `${logId}: missing envelopeTimestamp`);
strictAssert(envelopeTimestamp, `${logId}: missing envelopeId`);
log.info(
'delivery receipt from',
`${sourceServiceId || source}.${sourceDevice}`,
envelopeTimestamp,
'for sent message',
timestamp,
`wasSentEncrypted=${wasSentEncrypted}`
);
const syncTasks = deliveryReceipts
.map((deliveryReceipt): SyncTaskType | undefined => {
const {
sourceServiceId,
source,
sourceDevice,
timestamp,
wasSentEncrypted,
} = deliveryReceipt;
strictAssert(
envelopeTimestamp,
'onDeliveryReceipt: missing envelopeTimestamp'
);
strictAssert(
isServiceIdString(sourceServiceId),
'onDeliveryReceipt: missing valid sourceServiceId'
);
strictAssert(sourceDevice, 'onDeliveryReceipt: missing sourceDevice');
strictAssert(sourceConversation, 'onDeliveryReceipt: missing conversation');
const sourceConversation = window.ConversationController.lookupOrCreate(
{
serviceId: sourceServiceId,
e164: source,
reason: `onDeliveryReceipt(${envelopeTimestamp})`,
}
);
const attributes: MessageReceiptAttributesType = {
envelopeId: ev.deliveryReceipt.envelopeId,
removeFromMessageReceiverCache: ev.confirm,
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId: sourceConversation.id,
sourceServiceId,
sourceDevice,
type: MessageReceipts.MessageReceiptType.Delivery,
wasSentEncrypted,
};
log.info(
'delivery receipt from',
`${sourceServiceId || source}.${sourceDevice}`,
envelopeTimestamp,
'for sent message',
timestamp,
`wasSentEncrypted=${wasSentEncrypted}`
);
drop(MessageReceipts.onReceipt(attributes));
if (!isServiceIdString(sourceServiceId)) {
log.error(`${logId}: missing valid sourceServiceId`);
return undefined;
}
if (!sourceDevice) {
log.error(`${logId}: missing sourceDevice`);
return undefined;
}
if (!sourceConversation) {
log.error(`${logId}: missing conversation`);
return undefined;
}
const data: ReceiptSyncTaskType = {
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId: sourceConversation.id,
sourceServiceId,
sourceDevice,
type: MessageReceipts.messageReceiptTypeSchema.enum.Delivery,
wasSentEncrypted,
};
return {
id: generateUuid(),
attempts: 1,
createdAt: Date.now(),
data,
envelopeId,
sentAt: envelopeTimestamp,
type: 'Delivery',
};
})
.filter(isNotNil);
log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
await window.Signal.Data.saveSyncTasks(syncTasks);
confirm();
log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
log.info(`${logId}: Done`);
}
async function onDeleteForMeSync(ev: DeleteForMeSyncEvent) {