Move receipts and view/read syncs to new syncTasks system

This commit is contained in:
Scott Nonnenberg 2024-06-17 12:24:39 -07:00 committed by GitHub
parent 1a263e63da
commit 75c32e86f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1242 additions and 612 deletions

View file

@ -636,7 +636,7 @@ message SyncMessage {
message DeleteForMe {
message ConversationIdentifier {
oneof identifier {
string threadAci = 1;
string threadServiceId = 1;
bytes threadGroupId = 2;
string threadE164 = 3;
}
@ -644,7 +644,7 @@ message SyncMessage {
message AddressableMessage {
oneof author {
string authorAci = 1;
string authorServiceId = 1;
string authorE164 = 2;
}
optional uint64 sentTimestamp = 3;

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) {

View file

@ -1,6 +1,7 @@
// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { z } from 'zod';
import { groupBy } from 'lodash';
import type { MessageModel } from '../models/messages';
@ -10,7 +11,7 @@ import { isOutgoing, isStory } from '../state/selectors/message';
import { getOwn } from '../util/getOwn';
import { missingCaseError } from '../util/missingCaseError';
import { createWaitBatcher } from '../util/waitBatcher';
import type { ServiceIdString } from '../types/ServiceId';
import { isServiceIdString } from '../types/ServiceId';
import {
SendActionType,
SendStatus,
@ -23,7 +24,6 @@ import * as log from '../logging/log';
import { getSourceServiceId } from '../messages/helpers';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { getMessageIdForLogging } from '../util/idForLogging';
import { generateCacheKey } from './generateCacheKey';
import { getPropForTimestamp } from '../util/editHelpers';
import {
DELETE_SENT_PROTO_BATCHER_WAIT_MS,
@ -31,34 +31,30 @@ import {
} from '../types/Receipt';
import { drop } from '../util/drop';
const { deleteSentProtoRecipient } = dataInterface;
const { deleteSentProtoRecipient, removeSyncTaskById } = dataInterface;
export enum MessageReceiptType {
Delivery = 'Delivery',
Read = 'Read',
View = 'View',
}
export const messageReceiptTypeSchema = z.enum(['Delivery', 'Read', 'View']);
export type MessageReceiptType = z.infer<typeof messageReceiptTypeSchema>;
export const receiptSyncTaskSchema = z.object({
messageSentAt: z.number(),
receiptTimestamp: z.number(),
sourceConversationId: z.string(),
sourceDevice: z.number(),
sourceServiceId: z.string().refine(isServiceIdString),
type: messageReceiptTypeSchema,
wasSentEncrypted: z.boolean(),
});
export type ReceiptSyncTaskType = z.infer<typeof receiptSyncTaskSchema>;
export type MessageReceiptAttributesType = {
envelopeId: string;
messageSentAt: number;
receiptTimestamp: number;
removeFromMessageReceiverCache: () => void;
sourceConversationId: string;
sourceDevice: number;
sourceServiceId: ServiceIdString;
type: MessageReceiptType;
wasSentEncrypted: boolean;
syncTaskId: string;
receiptSync: ReceiptSyncTaskType;
};
function getReceiptCacheKey(receipt: MessageReceiptAttributesType): string {
return generateCacheKey({
sender: receipt.sourceServiceId,
timestamp: receipt.messageSentAt,
type: receipt.type,
});
}
const cachedReceipts = new Map<string, MessageReceiptAttributesType>();
const processReceiptBatcher = createWaitBatcher({
@ -69,7 +65,7 @@ const processReceiptBatcher = createWaitBatcher({
// First group by sentAt, so that we can find the target message
const receiptsByMessageSentAt = groupBy(
receipts,
receipt => receipt.messageSentAt
receipt => receipt.receiptSync.messageSentAt
);
// Once we find the message, we'll group them by messageId to process
@ -99,7 +95,7 @@ const processReceiptBatcher = createWaitBatcher({
continue;
}
// All receipts have the same sentAt, so we can grab it from the first
const sentAt = receiptsForMessageSentAt[0].messageSentAt;
const sentAt = receiptsForMessageSentAt[0].receiptSync.messageSentAt;
const messagesMatchingTimestamp =
// eslint-disable-next-line no-await-in-loop
@ -114,14 +110,16 @@ const processReceiptBatcher = createWaitBatcher({
if (reaction) {
for (const receipt of receiptsForMessageSentAt) {
const { receiptSync } = receipt;
log.info(
'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
receipt.messageSentAt,
receipt.type,
receipt.sourceConversationId,
receipt.sourceServiceId
receiptSync.messageSentAt,
receiptSync.type,
receiptSync.sourceConversationId,
receiptSync.sourceServiceId
);
remove(receipt);
// eslint-disable-next-line no-await-in-loop
await remove(receipt);
}
continue;
}
@ -129,7 +127,7 @@ const processReceiptBatcher = createWaitBatcher({
for (const receipt of receiptsForMessageSentAt) {
const targetMessage = getTargetMessage({
sourceConversationId: receipt.sourceConversationId,
sourceConversationId: receipt.receiptSync.sourceConversationId,
targetTimestamp: sentAt,
messagesMatchingTimestamp,
});
@ -144,7 +142,9 @@ const processReceiptBatcher = createWaitBatcher({
item.sendStateByConversationId &&
!item.deletedForEveryone &&
Boolean(
item.sendStateByConversationId[receipt.sourceConversationId]
item.sendStateByConversationId[
receipt.receiptSync.sourceConversationId
]
)
);
@ -154,12 +154,13 @@ const processReceiptBatcher = createWaitBatcher({
);
} else {
// Nope, no target message was found
const { receiptSync } = receipt;
log.info(
'MessageReceipts.processReceiptBatcher: No message for receipt',
receipt.messageSentAt,
receipt.type,
receipt.sourceConversationId,
receipt.sourceServiceId
receiptSync.messageSentAt,
receiptSync.type,
receiptSync.sourceConversationId,
receiptSync.sourceServiceId
);
}
}
@ -190,7 +191,7 @@ async function processReceiptsForMessage(
messageId
);
const { updatedMessage, validReceipts } = updateMessageWithReceipts(
const { updatedMessage, validReceipts } = await updateMessageWithReceipts(
message,
receipts
);
@ -204,7 +205,8 @@ async function processReceiptsForMessage(
// Confirm/remove receipts, and delete sent protos
for (const receipt of validReceipts) {
remove(receipt);
// eslint-disable-next-line no-await-in-loop
await remove(receipt);
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
}
@ -215,25 +217,27 @@ async function processReceiptsForMessage(
conversation?.debouncedUpdateLastMessage?.();
}
function updateMessageWithReceipts(
async function updateMessageWithReceipts(
message: MessageAttributesType,
receipts: Array<MessageReceiptAttributesType>
): {
): Promise<{
updatedMessage: MessageAttributesType;
validReceipts: Array<MessageReceiptAttributesType>;
} {
}> {
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
const toRemove: Array<MessageReceiptAttributesType> = [];
const receiptsToProcess = receipts.filter(receipt => {
if (shouldDropReceipt(receipt, message)) {
const { receiptSync } = receipt;
log.info(
`${logId}: Dropping a receipt ${receipt.type} for sentAt=${receipt.messageSentAt}`
`${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}`
);
remove(receipt);
toRemove.push(receipt);
return false;
}
if (!cachedReceipts.has(getReceiptCacheKey(receipt))) {
if (!cachedReceipts.has(receipt.syncTaskId)) {
// Between the time it was received and now, this receipt has already been handled!
return false;
}
@ -241,6 +245,8 @@ function updateMessageWithReceipts(
return true;
});
await Promise.all(toRemove.map(remove));
log.info(
`${logId}: batch processing ${receipts.length}` +
` receipt${receipts.length === 1 ? '' : 's'}`
@ -287,9 +293,10 @@ const deleteSentProtoBatcher = createWaitBatcher({
},
});
function remove(receipt: MessageReceiptAttributesType): void {
cachedReceipts.delete(getReceiptCacheKey(receipt));
receipt.removeFromMessageReceiverCache();
async function remove(receipt: MessageReceiptAttributesType): Promise<void> {
const { syncTaskId } = receipt;
cachedReceipts.delete(syncTaskId);
await removeSyncTaskById(syncTaskId);
}
function getTargetMessage({
@ -372,13 +379,13 @@ const shouldDropReceipt = (
receipt: MessageReceiptAttributesType,
message: MessageAttributesType
): boolean => {
const { type } = receipt;
const { type } = receipt.receiptSync;
switch (type) {
case MessageReceiptType.Delivery:
case messageReceiptTypeSchema.Enum.Delivery:
return false;
case MessageReceiptType.Read:
case messageReceiptTypeSchema.Enum.Read:
return !window.storage.get('read-receipt-setting');
case MessageReceiptType.View:
case messageReceiptTypeSchema.Enum.View:
if (isStory(message)) {
return !window.Events.getStoryViewReceiptsEnabled();
}
@ -388,9 +395,9 @@ const shouldDropReceipt = (
}
};
export function forMessage(
export async function forMessage(
message: MessageModel
): Array<MessageReceiptAttributesType> {
): Promise<Array<MessageReceiptAttributesType>> {
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
return [];
}
@ -408,20 +415,23 @@ export function forMessage(
const receiptValues = Array.from(cachedReceipts.values());
const sentAt = getMessageSentTimestamp(message.attributes, { log });
const result = receiptValues.filter(item => item.messageSentAt === sentAt);
const result = receiptValues.filter(
item => item.receiptSync.messageSentAt === sentAt
);
if (result.length > 0) {
log.info(`${logId}: found early receipts for message ${sentAt}`);
result.forEach(receipt => {
remove(receipt);
});
await Promise.all(
result.map(async receipt => {
await remove(receipt);
})
);
}
return result.filter(receipt => {
if (shouldDropReceipt(receipt, message.attributes)) {
log.info(
`${logId}: Dropping an early receipt ${receipt.type} for message ${sentAt}`
`${logId}: Dropping an early receipt ${receipt.receiptSync.type} for message ${sentAt}`
);
remove(receipt);
return false;
}
@ -433,7 +443,7 @@ function getNewSendStateByConversationId(
oldSendStateByConversationId: SendStateByConversationId,
receipt: MessageReceiptAttributesType
): SendStateByConversationId {
const { receiptTimestamp, sourceConversationId, type } = receipt;
const { receiptTimestamp, sourceConversationId, type } = receipt.receiptSync;
const oldSendState = getOwn(
oldSendStateByConversationId,
sourceConversationId
@ -441,13 +451,13 @@ function getNewSendStateByConversationId(
let sendActionType: SendActionType;
switch (type) {
case MessageReceiptType.Delivery:
case messageReceiptTypeSchema.enum.Delivery:
sendActionType = SendActionType.GotDeliveryReceipt;
break;
case MessageReceiptType.Read:
case messageReceiptTypeSchema.enum.Read:
sendActionType = SendActionType.GotReadReceipt;
break;
case MessageReceiptType.View:
case messageReceiptTypeSchema.enum.View:
sendActionType = SendActionType.GotViewedReceipt;
break;
default:
@ -467,7 +477,7 @@ function updateMessageSendStateWithReceipt(
message: MessageAttributesType,
receipt: MessageReceiptAttributesType
): Partial<MessageAttributesType> {
const { messageSentAt } = receipt;
const { messageSentAt } = receipt.receiptSync;
const newAttributes: Partial<MessageAttributesType> = {};
@ -510,27 +520,34 @@ async function addToDeleteSentProtoBatcher(
receipt: MessageReceiptAttributesType,
message: MessageAttributesType
) {
const { sourceConversationId, type } = receipt;
const { receiptSync } = receipt;
const {
sourceConversationId,
type,
wasSentEncrypted,
messageSentAt,
sourceDevice,
} = receiptSync;
if (
(type === MessageReceiptType.Delivery &&
(type === messageReceiptTypeSchema.enum.Delivery &&
wasDeliveredWithSealedSender(sourceConversationId, message) &&
receipt.wasSentEncrypted) ||
type === MessageReceiptType.Read
wasSentEncrypted) ||
type === messageReceiptTypeSchema.enum.Read
) {
const recipient = window.ConversationController.get(sourceConversationId);
const recipientServiceId = recipient?.getServiceId();
const deviceId = receipt.sourceDevice;
const deviceId = sourceDevice;
if (recipientServiceId && deviceId) {
await deleteSentProtoBatcher.add({
timestamp: receipt.messageSentAt,
timestamp: messageSentAt,
recipientServiceId,
deviceId,
});
} else {
log.warn(
`MessageReceipts.deleteSentProto(sentAt=${receipt.messageSentAt}): ` +
`MessageReceipts.deleteSentProto(sentAt=${messageSentAt}): ` +
`Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
);
}
@ -540,6 +557,6 @@ async function addToDeleteSentProtoBatcher(
export async function onReceipt(
receipt: MessageReceiptAttributesType
): Promise<void> {
cachedReceipts.set(getReceiptCacheKey(receipt), receipt);
cachedReceipts.set(receipt.syncTaskId, receipt);
await processReceiptBatcher.add(receipt);
}

View file

@ -1,7 +1,8 @@
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AciString } from '../types/ServiceId';
import { z } from 'zod';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
@ -14,58 +15,69 @@ import { isMessageUnread } from '../util/isMessageUnread';
import { notificationService } from '../services/notifications';
import { queueUpdateMessage } from '../util/messageBatcher';
import { strictAssert } from '../util/assert';
import { generateCacheKey } from './generateCacheKey';
import { isAciString } from '../util/isAciString';
import dataInterface from '../sql/Client';
const { removeSyncTaskById } = dataInterface;
export const readSyncTaskSchema = z.object({
type: z.literal('ReadSync').readonly(),
readAt: z.number(),
sender: z.string().optional(),
senderAci: z.string().refine(isAciString),
senderId: z.string(),
timestamp: z.number(),
});
export type ReadSyncTaskType = z.infer<typeof readSyncTaskSchema>;
export type ReadSyncAttributesType = {
envelopeId: string;
readAt: number;
removeFromMessageReceiverCache: () => unknown;
sender?: string;
senderAci: AciString;
senderId: string;
timestamp: number;
syncTaskId: string;
readSync: ReadSyncTaskType;
};
const readSyncs = new Map<string, ReadSyncAttributesType>();
function remove(sync: ReadSyncAttributesType): void {
readSyncs.delete(
generateCacheKey({
sender: sync.senderId,
timestamp: sync.timestamp,
type: 'readsync',
})
);
sync.removeFromMessageReceiverCache();
async function remove(sync: ReadSyncAttributesType): Promise<void> {
const { syncTaskId } = sync;
readSyncs.delete(syncTaskId);
await removeSyncTaskById(syncTaskId);
}
async function maybeItIsAReactionReadSync(
sync: ReadSyncAttributesType
): Promise<void> {
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
const { readSync } = sync;
const logId = `ReadSyncs.onSync(timestamp=${readSync.timestamp})`;
const readReaction = await window.Signal.Data.markReactionAsRead(
sync.senderAci,
Number(sync.timestamp)
readSync.senderAci,
Number(readSync.timestamp)
);
if (
!readReaction ||
readReaction?.targetAuthorAci !== window.storage.user.getCheckedAci()
) {
log.info(`${logId} not found:`, sync.senderId, sync.sender, sync.senderAci);
log.info(
`${logId} not found:`,
readSync.senderId,
readSync.sender,
readSync.senderAci
);
return;
}
log.info(
`${logId} read reaction sync found:`,
readReaction.conversationId,
sync.senderId,
sync.sender,
sync.senderAci
readSync.senderId,
readSync.sender,
readSync.senderAci
);
remove(sync);
await remove(sync);
notificationService.removeBy({
conversationId: readReaction.conversationId,
@ -75,9 +87,9 @@ async function maybeItIsAReactionReadSync(
});
}
export function forMessage(
export async function forMessage(
message: MessageModel
): ReadSyncAttributesType | null {
): Promise<ReadSyncAttributesType | null> {
const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
@ -92,13 +104,17 @@ export function forMessage(
});
const readSyncValues = Array.from(readSyncs.values());
const foundSync = readSyncValues.find(item => {
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
const { readSync } = item;
return (
readSync.senderId === sender?.id &&
readSync.timestamp === messageTimestamp
);
});
if (foundSync) {
log.info(
`${logId}: Found early read sync for message ${foundSync.timestamp}`
`${logId}: Found early read sync for message ${foundSync.readSync.timestamp}`
);
remove(foundSync);
await remove(foundSync);
return foundSync;
}
@ -106,20 +122,15 @@ export function forMessage(
}
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
readSyncs.set(
generateCacheKey({
sender: sync.senderId,
timestamp: sync.timestamp,
type: 'readsync',
}),
sync
);
const { readSync, syncTaskId } = sync;
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
readSyncs.set(syncTaskId, sync);
const logId = `ReadSyncs.onSync(timestamp=${readSync.timestamp})`;
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.timestamp
readSync.timestamp
);
const found = messages.find(item => {
@ -129,7 +140,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
reason: logId,
});
return isIncoming(item) && sender?.id === sync.senderId;
return isIncoming(item) && sender?.id === readSync.senderId;
});
if (!found) {
@ -144,8 +155,8 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
found,
'ReadSyncs.onSync'
);
const readAt = Math.min(sync.readAt, Date.now());
const newestSentAt = sync.timestamp;
const readAt = Math.min(readSync.readAt, Date.now());
const newestSentAt = readSync.timestamp;
// If message is unread, we mark it read. Otherwise, we update the expiration
// timer to the time specified by the read sync if it's earlier than
@ -193,9 +204,9 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
queueUpdateMessage(message.attributes);
remove(sync);
await remove(sync);
} catch (error) {
remove(sync);
log.error(`${logId} error:`, Errors.toLogFormat(error));
await remove(sync);
}
}

View file

@ -1,7 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AciString } from '../types/ServiceId';
import { z } from 'zod';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
@ -15,35 +16,38 @@ import { markViewed } from '../services/MessageUpdater';
import { notificationService } from '../services/notifications';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { queueUpdateMessage } from '../util/messageBatcher';
import { generateCacheKey } from './generateCacheKey';
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
import { isAciString } from '../util/isAciString';
import dataInterface from '../sql/Client';
const { removeSyncTaskById } = dataInterface;
export const viewSyncTaskSchema = z.object({
type: z.literal('ViewSync').readonly(),
senderAci: z.string().refine(isAciString),
senderE164: z.string().optional(),
senderId: z.string(),
timestamp: z.number(),
viewedAt: z.number(),
});
export type ViewSyncTaskType = z.infer<typeof viewSyncTaskSchema>;
export type ViewSyncAttributesType = {
envelopeId: string;
removeFromMessageReceiverCache: () => unknown;
senderAci: AciString;
senderE164?: string;
senderId: string;
timestamp: number;
viewedAt: number;
syncTaskId: string;
viewSync: ViewSyncTaskType;
};
const viewSyncs = new Map<string, ViewSyncAttributesType>();
function remove(sync: ViewSyncAttributesType): void {
viewSyncs.delete(
generateCacheKey({
sender: sync.senderId,
timestamp: sync.timestamp,
type: 'viewsync',
})
);
sync.removeFromMessageReceiverCache();
async function remove(sync: ViewSyncAttributesType): Promise<void> {
await removeSyncTaskById(sync.syncTaskId);
}
export function forMessage(
export async function forMessage(
message: MessageModel
): Array<ViewSyncAttributesType> {
): Promise<Array<ViewSyncAttributesType>> {
const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
@ -60,7 +64,11 @@ export function forMessage(
const viewSyncValues = Array.from(viewSyncs.values());
const matchingSyncs = viewSyncValues.filter(item => {
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
const { viewSync } = item;
return (
viewSync.senderId === sender?.id &&
viewSync.timestamp === messageTimestamp
);
});
if (matchingSyncs.length > 0) {
@ -68,28 +76,24 @@ export function forMessage(
`${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
);
}
matchingSyncs.forEach(sync => {
remove(sync);
});
await Promise.all(
matchingSyncs.map(async sync => {
await remove(sync);
})
);
return matchingSyncs;
}
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
viewSyncs.set(
generateCacheKey({
sender: sync.senderId,
timestamp: sync.timestamp,
type: 'viewsync',
}),
sync
);
viewSyncs.set(sync.syncTaskId, sync);
const { viewSync } = sync;
const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
const logId = `ViewSyncs.onSync(timestamp=${viewSync.timestamp})`;
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.timestamp
viewSync.timestamp
);
const found = messages.find(item => {
@ -99,15 +103,15 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
reason: logId,
});
return sender?.id === sync.senderId;
return sender?.id === viewSync.senderId;
});
if (!found) {
log.info(
`${logId}: nothing found`,
sync.senderId,
sync.senderE164,
sync.senderAci
viewSync.senderId,
viewSync.senderE164,
viewSync.senderAci
);
return;
}
@ -123,7 +127,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
if (message.get('readStatus') !== ReadStatus.Viewed) {
didChangeMessage = true;
message.set(markViewed(message.attributes, sync.viewedAt));
message.set(markViewed(message.attributes, viewSync.viewedAt));
const attachments = message.get('attachments');
if (!attachments?.every(isDownloaded)) {
@ -154,9 +158,9 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
queueUpdateMessage(message.attributes);
}
remove(sync);
await remove(sync);
} catch (error) {
remove(sync);
log.error(`${logId} error:`, Errors.toLogFormat(error));
await remove(sync);
}
}

View file

@ -2945,6 +2945,13 @@ export class ConversationModel extends window.Backbone
senderAci,
});
if (!this.get('active_at')) {
log.warn(
`addDeliveryIssue: ${this.idForLogging()} has no active_at, dropping delivery issue instead of adding`
);
return;
}
const message = {
conversationId: this.id,
type: 'delivery-issue',
@ -3363,7 +3370,9 @@ export class ConversationModel extends window.Backbone
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
if (message) {
await window.Signal.Data.removeMessage(message.id);
await window.Signal.Data.removeMessage(message.id, {
singleProtoJobQueue,
});
}
return true;
}
@ -3404,7 +3413,9 @@ export class ConversationModel extends window.Backbone
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
if (message) {
await window.Signal.Data.removeMessage(message.id);
await window.Signal.Data.removeMessage(message.id, {
singleProtoJobQueue,
});
}
return true;
@ -5003,7 +5014,14 @@ export class ConversationModel extends window.Backbone
});
window.Signal.Data.updateConversation(this.attributes);
if (source === 'local-delete' && isEnabled('desktop.deleteSync.send')) {
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
const capable = Boolean(ourConversation.get('capabilities')?.deleteSync);
if (
source === 'local-delete' &&
capable &&
isEnabled('desktop.deleteSync.send')
) {
log.info(`${logId}: Preparing sync message`);
const timestamp = Date.now();
@ -5044,7 +5062,9 @@ export class ConversationModel extends window.Backbone
log.info(`${logId}: Starting delete`);
await window.Signal.Data.removeMessagesInConversation(this.id, {
fromSync: source !== 'local-delete-sync',
logId: this.idForLogging(),
singleProtoJobQueue,
});
log.info(`${logId}: Delete complete`);
}

View file

@ -109,7 +109,7 @@ import {
} from '../services/notifications';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as log from '../logging/log';
import { cleanupMessage, deleteMessageData } from '../util/cleanup';
import { deleteMessageData } from '../util/cleanup';
import {
getSource,
getSourceServiceId,
@ -315,10 +315,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.set(attributes);
}
async cleanup(): Promise<void> {
await cleanupMessage(this.attributes);
}
async deleteData(): Promise<void> {
await deleteMessageData(this.attributes);
}

View file

@ -4,18 +4,21 @@
import { batch } from 'react-redux';
import { debounce } from 'lodash';
import type { MessageModel } from '../models/messages';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { sleep } from '../util/sleep';
import { SECOND } from '../util/durations';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import type { MessageModel } from '../models/messages';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
class ExpiringMessagesDeletionService {
public update: typeof this.checkExpiringMessages;
private timeout?: ReturnType<typeof setTimeout>;
constructor() {
constructor(private readonly singleProtoJobQueue: SingleProtoJobQueue) {
this.update = debounce(this.checkExpiringMessages, 1000);
}
@ -42,7 +45,9 @@ class ExpiringMessagesDeletionService {
inMemoryMessages.push(message);
});
await window.Signal.Data.removeMessages(messageIds);
await window.Signal.Data.removeMessages(messageIds, {
singleProtoJobQueue: this.singleProtoJobQueue,
});
batch(() => {
inMemoryMessages.forEach(message => {
@ -108,5 +113,21 @@ class ExpiringMessagesDeletionService {
}
}
export const expiringMessagesDeletionService =
new ExpiringMessagesDeletionService();
// Because this service is used inside of Client.ts, it can't directly reference
// SingleProtoJobQueue. Instead of direct access, it is provided once on startup.
export function initialize(singleProtoJobQueue: SingleProtoJobQueue): void {
if (instance) {
log.warn('Expiring Messages Deletion service is already initialized!');
return;
}
instance = new ExpiringMessagesDeletionService(singleProtoJobQueue);
}
export async function update(): Promise<void> {
if (!instance) {
throw new Error('Expiring Messages Deletion service not yet initialized!');
}
await instance.update();
}
let instance: ExpiringMessagesDeletionService;

View file

@ -2,13 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer as ipc } from 'electron';
import PQueue from 'p-queue';
import { batch } from 'react-redux';
import { has, get, groupBy, isTypedArray, last, map, omit } from 'lodash';
import { deleteExternalFiles } from '../types/Conversation';
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
import * as Bytes from '../Bytes';
import { createBatcher } from '../util/batcher';
@ -24,12 +22,7 @@ import * as Errors from '../types/errors';
import type { StoredJob } from '../jobs/types';
import { formatJobForInsert } from '../jobs/formatJobForInsert';
import {
cleanupMessage,
cleanupMessageFromMemory,
deleteMessageData,
} from '../util/cleanup';
import { drop } from '../util/drop';
import { cleanupMessages } from '../util/cleanup';
import { ipcInvoke, doShutdown } from './channels';
import type {
@ -60,12 +53,12 @@ import type {
KyberPreKeyType,
StoredKyberPreKeyType,
} from './Interface';
import { MINUTE } from '../util/durations';
import { getMessageIdForLogging } from '../util/idForLogging';
import type { MessageAttributesType } from '../model-types';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateSnippetAroundMention } from '../util/search';
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
@ -104,6 +97,8 @@ const exclusiveInterface: ClientExclusiveInterface = {
removeConversation,
searchMessages,
removeMessage,
removeMessages,
getRecentStoryReplies,
getOlderMessagesByConversation,
@ -125,8 +120,6 @@ const exclusiveInterface: ClientExclusiveInterface = {
type ClientOverridesType = ClientExclusiveInterface &
Pick<
ServerInterface,
| 'removeMessage'
| 'removeMessages'
| 'saveAttachmentDownloadJob'
| 'saveMessage'
| 'saveMessages'
@ -142,8 +135,6 @@ const channels: ServerInterface = new Proxy({} as ServerInterface, {
const clientExclusiveOverrides: ClientOverridesType = {
...exclusiveInterface,
removeMessage,
removeMessages,
saveAttachmentDownloadJob,
saveMessage,
saveMessages,
@ -562,7 +553,7 @@ async function saveMessage(
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
void expiringMessagesDeletionService.update();
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
return id;
@ -577,26 +568,39 @@ async function saveMessages(
options
);
void expiringMessagesDeletionService.update();
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
return result;
}
async function removeMessage(id: string): Promise<void> {
async function removeMessage(
id: string,
options: {
singleProtoJobQueue: SingleProtoJobQueue;
fromSync?: boolean;
}
): Promise<void> {
const message = await channels.getMessageById(id);
// Note: It's important to have a fully database-hydrated model to delete here because
// it needs to delete all associated on-disk files along with the database delete.
if (message) {
await channels.removeMessage(id);
await cleanupMessage(message);
await cleanupMessages([message], {
...options,
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
});
}
}
export async function deleteAndCleanup(
messages: Array<MessageAttributesType>,
logId: string
logId: string,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
const ids = messages.map(message => message.id);
@ -604,37 +608,26 @@ export async function deleteAndCleanup(
await channels.removeMessages(ids);
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
await _cleanupMessages(messages);
await cleanupMessages(messages, {
...options,
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
});
log.info(`deleteAndCleanup/${logId}: Complete`);
}
async function _cleanupMessages(
messages: ReadonlyArray<MessageAttributesType>
): Promise<void> {
// First, remove messages from memory, so we can batch the updates in redux
batch(() => {
messages.forEach(message => cleanupMessageFromMemory(message));
});
// Then, handle any asynchronous actions (e.g. deleting data from disk)
const queue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
drop(
queue.addAll(
messages.map(
(message: MessageAttributesType) => async () =>
deleteMessageData(message)
)
)
);
await queue.onIdle();
}
async function removeMessages(
messageIds: ReadonlyArray<string>
messageIds: ReadonlyArray<string>,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
const messages = await channels.getMessagesById(messageIds);
await _cleanupMessages(messages);
await cleanupMessages(messages, {
...options,
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
});
await channels.removeMessages(messageIds);
}
@ -686,9 +679,13 @@ async function removeMessagesInConversation(
{
logId,
receivedAt,
singleProtoJobQueue,
fromSync,
}: {
fromSync?: boolean;
logId: string;
receivedAt?: number;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
let messages;
@ -713,7 +710,7 @@ async function removeMessagesInConversation(
}
// eslint-disable-next-line no-await-in-loop
await deleteAndCleanup(messages, logId);
await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue });
} while (messages.length > 0);
}

View file

@ -34,6 +34,7 @@ import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
import type { SyncTaskType } from '../util/syncTasks';
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
export type AdjacentMessagesByConversationOptionsType = Readonly<{
conversationId: string;
@ -557,8 +558,6 @@ export type DataInterface = {
arrayOfMessages: ReadonlyArray<MessageType>,
options: { forceSave?: boolean; ourAci: AciString }
) => Promise<Array<string>>;
removeMessage: (id: string) => Promise<void>;
removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
pageMessages: (
cursor?: PageMessagesCursorType
) => Promise<PageMessagesResultType>;
@ -667,6 +666,7 @@ export type DataInterface = {
conversationId: string;
}): Promise<MessageType | undefined>;
getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
markCallHistoryDeleted: (callId: string) => Promise<void>;
clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
cleanupCallHistoryMessages: () => Promise<void>;
getCallHistoryUnreadCount(): Promise<number>;
@ -929,6 +929,8 @@ export type ServerInterface = DataInterface & {
options?: { limit?: number };
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
}) => Promise<Array<ServerSearchResultMessageType>>;
removeMessage: (id: string) => Promise<void>;
removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
getRecentStoryReplies(
storyId: string,
@ -1022,6 +1024,20 @@ export type ClientExclusiveInterface = {
removeConversation: (id: string) => Promise<void>;
flushUpdateConversationBatcher: () => Promise<void>;
removeMessage: (
id: string,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
) => Promise<void>;
removeMessages: (
ids: ReadonlyArray<string>,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
) => Promise<void>;
searchMessages: ({
query,
conversationId,
@ -1084,8 +1100,10 @@ export type ClientExclusiveInterface = {
removeMessagesInConversation: (
conversationId: string,
options: {
fromSync?: boolean;
logId: string;
receivedAt?: number;
singleProtoJobQueue: SingleProtoJobQueue;
}
) => Promise<void>;
removeOtherData: () => Promise<void>;

View file

@ -347,6 +347,7 @@ const dataInterface: ServerInterface = {
getLastConversationMessage,
getAllCallHistory,
clearCallHistory,
markCallHistoryDeleted,
cleanupCallHistoryMessages,
getCallHistoryUnreadCount,
markCallHistoryRead,
@ -3635,6 +3636,19 @@ async function clearCallHistory(
})();
}
async function markCallHistoryDeleted(callId: string): Promise<void> {
const db = await getWritableInstance();
const [query, params] = sql`
UPDATE callsHistory
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callId = ${callId};
`;
db.prepare(query).run(params);
}
async function cleanupCallHistoryMessages(): Promise<void> {
const db = await getWritableInstance();
return db

View file

@ -195,6 +195,7 @@ import {
} from '../../util/deleteForMe';
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
import { isEnabled } from '../../RemoteConfig';
import type { CapabilitiesType } from '../../textsecure/WebAPI';
// State
@ -265,6 +266,7 @@ export type ConversationType = ReadonlyDeep<
firstName?: string;
profileName?: string;
profileLastUpdatedAt?: number;
capabilities?: CapabilitiesType;
username?: string;
about?: string;
aboutText?: string;
@ -1753,7 +1755,9 @@ function deleteMessages({
}
}
await window.Signal.Data.removeMessages(messageIds);
await window.Signal.Data.removeMessages(messageIds, {
singleProtoJobQueue,
});
popPanelForConversation()(dispatch, getState, undefined);
@ -1761,7 +1765,11 @@ function deleteMessages({
dispatch(scrollToMessage(conversationId, nearbyMessageId));
}
if (!isEnabled('desktop.deleteSync.send')) {
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
const capable = Boolean(ourConversation.get('capabilities')?.deleteSync);
if (!capable || !isEnabled('desktop.deleteSync.send')) {
return;
}
if (messages.length === 0) {

View file

@ -69,6 +69,7 @@ import {
conversationQueueJobEnum,
} from '../../jobs/conversationJobQueue';
import { ReceiptType } from '../../types/Receipt';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
export type StoryDataType = ReadonlyDeep<
{
@ -284,7 +285,7 @@ function deleteGroupStoryReply(
messageId: string
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
return async dispatch => {
await window.Signal.Data.removeMessage(messageId);
await window.Signal.Data.removeMessage(messageId, { singleProtoJobQueue });
dispatch({
type: STORY_REPLY_DELETED,
payload: messageId,
@ -1408,10 +1409,7 @@ function removeAllContactStories(
log.info(`${logId}: removing ${messages.length} stories`);
await Promise.all([
messages.map(m => m.cleanup()),
await dataInterface.removeMessages(messageIds),
]);
await dataInterface.removeMessages(messageIds, { singleProtoJobQueue });
dispatch({
type: 'NOOP',

View file

@ -0,0 +1,38 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import { getUserACI } from './user';
import { getConversationSelector } from './conversations';
import { getRemoteConfig, isRemoteConfigFlagEnabled } from './items';
import type { AciString } from '../../types/ServiceId';
import type { ConfigMapType } from '../../RemoteConfig';
import type { GetConversationByIdType } from './conversations';
export const getDeleteSyncSendEnabled = createSelector(
getUserACI,
getConversationSelector,
getRemoteConfig,
(
aci: AciString | undefined,
conversationSelector: GetConversationByIdType,
remoteConfig: ConfigMapType
): boolean => {
if (!aci) {
return false;
}
const ourConversation = conversationSelector(aci);
if (!ourConversation) {
return false;
}
const { capabilities } = ourConversation;
if (!capabilities || !capabilities.deleteSync) {
return false;
}
return isRemoteConfigFlagEnabled(remoteConfig, 'desktop.deleteSync.send');
}
);

View file

@ -127,13 +127,6 @@ export const isInternalUser = createSelector(
}
);
export const getDeleteSyncSendEnabled = createSelector(
getRemoteConfig,
(remoteConfig: ConfigMapType): boolean => {
return isRemoteConfigFlagEnabled(remoteConfig, 'desktop.deleteSync.send');
}
);
// Note: ts/util/stories is the other place this check is done
export const getStoriesEnabled = createSelector(
getItems,

View file

@ -41,10 +41,8 @@ import {
import { getHasStoriesSelector } from '../selectors/stories2';
import { getIntl, getTheme, getUserACI } from '../selectors/user';
import { useItemsActions } from '../ducks/items';
import {
getDeleteSyncSendEnabled,
getLocalDeleteWarningShown,
} from '../selectors/items';
import { getLocalDeleteWarningShown } from '../selectors/items';
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
export type OwnProps = {
id: string;

View file

@ -17,10 +17,8 @@ import {
} from '../selectors/conversations';
import { getDeleteMessagesProps } from '../selectors/globalModals';
import { useItemsActions } from '../ducks/items';
import {
getLocalDeleteWarningShown,
getDeleteSyncSendEnabled,
} from '../selectors/items';
import { getLocalDeleteWarningShown } from '../selectors/items';
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
export const SmartDeleteMessagesModal = memo(

View file

@ -59,9 +59,9 @@ describe('filterAndSortConversations', () => {
check({
searchTerm: '9876',
input: [
{ title: 'no' },
{ title: 'no', e164: undefined },
{ title: 'yes', e164: '+16505559876' },
{ title: 'no' },
{ title: 'no', e164: undefined },
],
expected: [{ title: 'yes' }],
});

View file

@ -7,10 +7,13 @@ import { assert } from 'chai';
import { type AciString, generateAci } from '../types/ServiceId';
import type { MessageAttributesType } from '../model-types';
import { SendStatus } from '../messages/MessageSendState';
import {
type MessageReceiptAttributesType,
import type {
MessageReceiptAttributesType,
MessageReceiptType,
} from '../messageModifiers/MessageReceipts';
import {
onReceipt,
messageReceiptTypeSchema,
} from '../messageModifiers/MessageReceipts';
import { ReadStatus } from '../messages/MessageReadStatus';
@ -31,14 +34,16 @@ describe('MessageReceipts', () => {
): MessageReceiptAttributesType {
return {
envelopeId: uuid(),
messageSentAt,
receiptTimestamp: 1,
removeFromMessageReceiverCache: () => null,
sourceConversationId,
sourceDevice: 1,
sourceServiceId: generateAci(),
type,
wasSentEncrypted: true,
syncTaskId: uuid(),
receiptSync: {
messageSentAt,
receiptTimestamp: 1,
sourceConversationId,
sourceDevice: 1,
sourceServiceId: generateAci(),
type,
wasSentEncrypted: true,
},
};
}
it('processes all receipts in a batch', async () => {
@ -78,10 +83,18 @@ describe('MessageReceipts', () => {
});
await Promise.all([
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
onReceipt(
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
),
onReceipt(
generateReceipt('bbbb', sentAt, messageReceiptTypeSchema.enum.Delivery)
),
onReceipt(
generateReceipt('cccc', sentAt, messageReceiptTypeSchema.enum.Read)
),
onReceipt(
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Read)
),
]);
const messageFromDatabase = await window.Signal.Data.getMessageById(id);
@ -154,20 +167,48 @@ describe('MessageReceipts', () => {
await Promise.all([
// send receipts for original message
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
onReceipt(
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
),
onReceipt(
generateReceipt('bbbb', sentAt, messageReceiptTypeSchema.enum.Delivery)
),
onReceipt(
generateReceipt('cccc', sentAt, messageReceiptTypeSchema.enum.Read)
),
onReceipt(
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Read)
),
// and send receipts for edited message
onReceipt(
generateReceipt('aaaa', editedSentAt, MessageReceiptType.Delivery)
generateReceipt(
'aaaa',
editedSentAt,
messageReceiptTypeSchema.enum.Delivery
)
),
onReceipt(
generateReceipt('bbbb', editedSentAt, MessageReceiptType.Delivery)
generateReceipt(
'bbbb',
editedSentAt,
messageReceiptTypeSchema.enum.Delivery
)
),
onReceipt(
generateReceipt(
'cccc',
editedSentAt,
messageReceiptTypeSchema.enum.Read
)
),
onReceipt(
generateReceipt(
'bbbb',
editedSentAt,
messageReceiptTypeSchema.enum.Read
)
),
onReceipt(generateReceipt('cccc', editedSentAt, MessageReceiptType.Read)),
onReceipt(generateReceipt('bbbb', editedSentAt, MessageReceiptType.Read)),
]);
const messageFromDatabase = await window.Signal.Data.getMessageById(id);

View file

@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
import dataInterface from '../../sql/Client';
import { generateAci } from '../../types/ServiceId';
import { constantTimeEqual, getRandomBytes } from '../../Crypto';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
const {
_getAllSentProtoMessageIds,
@ -148,7 +149,7 @@ describe('sql/sendLog', () => {
assert.strictEqual(actual.timestamp, proto.timestamp);
await removeMessage(id);
await removeMessage(id, { singleProtoJobQueue });
assert.lengthOf(await getAllSentProtos(), 0);
});

View file

@ -11,6 +11,7 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
import type { ConversationModel } from '../../models/conversations';
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
import * as Bytes from '../../Bytes';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
describe('KeyChangeListener', () => {
let oldNumberId: string | undefined;
@ -69,6 +70,7 @@ describe('KeyChangeListener', () => {
afterEach(async () => {
await window.Signal.Data.removeMessagesInConversation(convo.id, {
logId: ourServiceIdWithKeyChange,
singleProtoJobQueue,
});
await window.Signal.Data.removeConversation(convo.id);
@ -106,6 +108,7 @@ describe('KeyChangeListener', () => {
afterEach(async () => {
await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
logId: ourServiceIdWithKeyChange,
singleProtoJobQueue,
});
await window.Signal.Data.removeConversation(groupConvo.id);
});

View file

@ -137,6 +137,8 @@ import type {
DeleteForMeSyncEventData,
DeleteForMeSyncTarget,
ConversationToDelete,
ViewSyncEventData,
ReadSyncEventData,
} from './messageReceiverEvents';
import * as log from '../logging/log';
import * as durations from '../util/durations';
@ -1728,15 +1730,17 @@ export default class MessageReceiver
await this.dispatchAndWait(
getEnvelopeId(envelope),
new DeliveryEvent(
{
envelopeId: envelope.id,
timestamp: envelope.timestamp,
envelopeTimestamp: envelope.timestamp,
source: envelope.source,
sourceServiceId: envelope.sourceServiceId,
sourceDevice: envelope.sourceDevice,
wasSentEncrypted: false,
},
[
{
timestamp: envelope.timestamp,
source: envelope.source,
sourceServiceId: envelope.sourceServiceId,
sourceDevice: envelope.sourceDevice,
wasSentEncrypted: false,
},
],
envelope.id,
envelope.timestamp,
this.removeFromCache.bind(this, envelope)
)
);
@ -2907,22 +2911,22 @@ export default class MessageReceiver
const logId = getEnvelopeId(envelope);
await Promise.all(
receiptMessage.timestamp.map(async rawTimestamp => {
const ev = new EventClass(
{
envelopeId: envelope.id,
timestamp: rawTimestamp?.toNumber(),
envelopeTimestamp: envelope.timestamp,
source: envelope.source,
sourceServiceId: envelope.sourceServiceId,
sourceDevice: envelope.sourceDevice,
wasSentEncrypted: true,
},
this.removeFromCache.bind(this, envelope)
);
await this.dispatchAndWait(logId, ev);
})
const receipts = receiptMessage.timestamp.map(rawTimestamp => ({
timestamp: rawTimestamp?.toNumber(),
source: envelope.source,
sourceServiceId: envelope.sourceServiceId,
sourceDevice: envelope.sourceDevice,
wasSentEncrypted: true as const,
}));
await this.dispatchAndWait(
logId,
new EventClass(
receipts,
envelope.id,
envelope.timestamp,
this.removeFromCache.bind(this, envelope)
)
);
}
@ -3469,23 +3473,27 @@ export default class MessageReceiver
logUnexpectedUrgentValue(envelope, 'readSync');
const results = [];
for (const { timestamp, sender, senderAci } of read) {
const ev = new ReadSyncEvent(
{
envelopeId: envelope.id,
envelopeTimestamp: envelope.timestamp,
timestamp: timestamp?.toNumber(),
sender: dropNull(sender),
senderAci: senderAci
? normalizeAci(senderAci, 'handleRead.senderAci')
: undefined,
},
const reads = read.map(
({ timestamp, sender, senderAci }): ReadSyncEventData => ({
envelopeId: envelope.id,
envelopeTimestamp: envelope.timestamp,
timestamp: timestamp?.toNumber(),
sender: dropNull(sender),
senderAci: senderAci
? normalizeAci(senderAci, 'handleRead.senderAci')
: undefined,
})
);
await this.dispatchAndWait(
logId,
new ReadSyncEvent(
reads,
envelope.id,
envelope.timestamp,
this.removeFromCache.bind(this, envelope)
);
results.push(this.dispatchAndWait(logId, ev));
}
await Promise.all(results);
)
);
}
private async handleViewed(
@ -3497,23 +3505,25 @@ export default class MessageReceiver
logUnexpectedUrgentValue(envelope, 'viewSync');
await Promise.all(
viewed.map(async ({ timestamp, senderE164, senderAci }) => {
const ev = new ViewSyncEvent(
{
envelopeId: envelope.id,
envelopeTimestamp: envelope.timestamp,
timestamp: timestamp?.toNumber(),
senderE164: dropNull(senderE164),
senderAci: senderAci
? normalizeAci(senderAci, 'handleViewed.senderAci')
: undefined,
},
this.removeFromCache.bind(this, envelope)
);
await this.dispatchAndWait(logId, ev);
const views = viewed.map(
({ timestamp, senderE164, senderAci }): ViewSyncEventData => ({
timestamp: timestamp?.toNumber(),
senderE164: dropNull(senderE164),
senderAci: senderAci
? normalizeAci(senderAci, 'handleViewed.senderAci')
: undefined,
})
);
await this.dispatchAndWait(
logId,
new ViewSyncEvent(
views,
envelope.id,
envelope.timestamp,
this.removeFromCache.bind(this, envelope)
)
);
}
private async handleCallEvent(
@ -3663,19 +3673,28 @@ export default class MessageReceiver
? processConversationToDelete(item.conversation, logId)
: undefined;
if (messages?.length && conversation) {
// We want each message in its own task
return messages.map(innerItem => {
return {
type: 'delete-message' as const,
message: innerItem,
conversation,
timestamp,
};
});
if (!conversation) {
log.warn(
`${logId}/handleDeleteForMeSync/messageDeletes: No target conversation`
);
return undefined;
}
if (!messages?.length) {
log.warn(
`${logId}/handleDeleteForMeSync/messageDeletes: No target messages`
);
return undefined;
}
return undefined;
// We want each message in its own task
return messages.map(innerItem => {
return {
type: 'delete-message' as const,
message: innerItem,
conversation,
timestamp,
};
});
})
.filter(isNotNil);
@ -3692,17 +3711,26 @@ export default class MessageReceiver
? processConversationToDelete(item.conversation, logId)
: undefined;
if (mostRecentMessages?.length && conversation) {
return {
type: 'delete-conversation' as const,
conversation,
isFullDelete: Boolean(item.isFullDelete),
mostRecentMessages,
timestamp,
};
if (!conversation) {
log.warn(
`${logId}/handleDeleteForMeSync/conversationDeletes: No target conversation`
);
return undefined;
}
if (!mostRecentMessages?.length) {
log.warn(
`${logId}/handleDeleteForMeSync/conversationDeletes: No target messages`
);
return undefined;
}
return undefined;
return {
type: 'delete-conversation' as const,
conversation,
isFullDelete: Boolean(item.isFullDelete),
mostRecentMessages,
timestamp,
};
})
.filter(isNotNil);
@ -3716,15 +3744,18 @@ export default class MessageReceiver
? processConversationToDelete(item.conversation, logId)
: undefined;
if (conversation) {
return {
type: 'delete-local-conversation' as const,
conversation,
timestamp,
};
if (!conversation) {
log.warn(
`${logId}/handleDeleteForMeSync/localOnlyConversationDeletes: No target conversation`
);
return undefined;
}
return undefined;
return {
type: 'delete-local-conversation' as const,
conversation,
timestamp,
};
})
.filter(isNotNil);
@ -3969,15 +4000,32 @@ function processMessageToDelete(
return undefined;
}
if (target.authorAci) {
return {
type: 'aci' as const,
authorAci: normalizeAci(
target.authorAci,
`${logId}/processMessageToDelete`
),
sentAt,
};
const { authorServiceId } = target;
if (authorServiceId) {
if (isAciString(authorServiceId)) {
return {
type: 'aci' as const,
authorAci: normalizeAci(
authorServiceId,
`${logId}/processMessageToDelete/aci`
),
sentAt,
};
}
if (isPniString(authorServiceId)) {
return {
type: 'pni' as const,
authorPni: normalizePni(
authorServiceId,
`${logId}/processMessageToDelete/pni`
),
sentAt,
};
}
log.error(
`${logId}/processMessageToDelete: invalid authorServiceId, Dropping AddressableMessage.`
);
return undefined;
}
if (target.authorE164) {
return {
@ -3997,13 +4045,25 @@ function processConversationToDelete(
target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
logId: string
): ConversationToDelete | undefined {
const { threadAci, threadGroupId, threadE164 } = target;
const { threadServiceId, threadGroupId, threadE164 } = target;
if (threadAci) {
return {
type: 'aci' as const,
aci: normalizeAci(threadAci, `${logId}/threadAci`),
};
if (threadServiceId) {
if (isAciString(threadServiceId)) {
return {
type: 'aci' as const,
aci: normalizeAci(threadServiceId, `${logId}/aci`),
};
}
if (isPniString(threadServiceId)) {
return {
type: 'pni' as const,
pni: normalizePni(threadServiceId, `${logId}/pni`),
};
}
log.error(
`${logId}/processConversationToDelete: Invalid threadServiceId, dropping ConversationIdentifier.`
);
return undefined;
}
if (threadGroupId) {
return {

View file

@ -89,6 +89,14 @@ import type {
MessageToDelete,
} from './messageReceiverEvents';
import { getConversationFromTarget } from '../util/deleteForMe';
import type { CallDetails } from '../types/CallDisposition';
import {
AdhocCallStatus,
DirectCallStatus,
GroupCallStatus,
} from '../types/CallDisposition';
import { getProtoForCallHistory } from '../util/callDisposition';
import { CallMode } from '../types/Calling';
export type SendMetadataType = {
[serviceId: ServiceIdString]: {
@ -1567,6 +1575,71 @@ export default class MessageSender {
};
}
static getClearCallHistoryMessage(timestamp: number): SingleProtoJobData {
const ourAci = window.textsecure.storage.user.getCheckedAci();
const callLogEvent = new Proto.SyncMessage.CallLogEvent({
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
timestamp: Long.fromNumber(timestamp),
});
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callLogEvent = callLogEvent;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return {
contentHint: ContentHint.RESENDABLE,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
),
type: 'callLogEventSync',
urgent: false,
};
}
static getDeleteCallEvent(callDetails: CallDetails): SingleProtoJobData {
const ourAci = window.textsecure.storage.user.getCheckedAci();
const { mode } = callDetails;
let status;
if (mode === CallMode.Adhoc) {
status = AdhocCallStatus.Deleted;
} else if (mode === CallMode.Direct) {
status = DirectCallStatus.Deleted;
} else if (mode === CallMode.Group) {
status = GroupCallStatus.Deleted;
} else {
throw missingCaseError(mode);
}
const callEvent = getProtoForCallHistory({
...callDetails,
status,
});
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callEvent = callEvent;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return {
contentHint: ContentHint.RESENDABLE,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
),
type: 'callLogEventSync',
urgent: false,
};
}
async syncReadMessages(
reads: ReadonlyArray<{
senderAci?: AciString;
@ -2353,9 +2426,11 @@ function toAddressableMessage(message: MessageToDelete) {
targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
if (message.type === 'aci') {
targetMessage.authorAci = message.authorAci;
targetMessage.authorServiceId = message.authorAci;
} else if (message.type === 'e164') {
targetMessage.authorE164 = message.authorE164;
} else if (message.type === 'pni') {
targetMessage.authorServiceId = message.authorPni;
} else {
throw missingCaseError(message);
}
@ -2368,7 +2443,9 @@ function toConversationIdentifier(conversation: ConversationToDelete) {
new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
if (conversation.type === 'aci') {
targetConversation.threadAci = conversation.aci;
targetConversation.threadServiceId = conversation.aci;
} else if (conversation.type === 'pni') {
targetConversation.threadServiceId = conversation.pni;
} else if (conversation.type === 'group') {
targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
} else if (conversation.type === 'e164') {

View file

@ -703,8 +703,12 @@ export type WebAPIConnectType = {
connect: (options: WebAPIConnectOptionsType) => WebAPIType;
};
export type CapabilitiesType = Record<string, never>;
export type CapabilitiesUploadType = Record<string, never>;
export type CapabilitiesType = {
deleteSync: boolean;
};
export type CapabilitiesUploadType = {
deleteSync: true;
};
type StickerPackManifestType = Uint8Array;

View file

@ -6,7 +6,11 @@ import type { PublicKey } from '@signalapp/libsignal-client';
import { z } from 'zod';
import type { SignalService as Proto } from '../protobuf';
import type { ServiceIdString, AciString } from '../types/ServiceId';
import {
type ServiceIdString,
type AciString,
isPniString,
} from '../types/ServiceId';
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type {
ProcessedEnvelope,
@ -93,7 +97,6 @@ export class EnvelopeUnsealedEvent extends Event {
}
}
// Emitted when we queue previously-decrypted events from the cache
export class EnvelopeQueuedEvent extends Event {
constructor(public readonly envelope: ProcessedEnvelope) {
super('envelopeQueued');
@ -113,9 +116,7 @@ export class ConfirmableEvent extends Event {
}
export type DeliveryEventData = Readonly<{
envelopeId: string;
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceServiceId?: ServiceIdString;
sourceDevice?: number;
@ -124,7 +125,9 @@ export type DeliveryEventData = Readonly<{
export class DeliveryEvent extends ConfirmableEvent {
constructor(
public readonly deliveryReceipt: DeliveryEventData,
public readonly deliveryReceipts: ReadonlyArray<DeliveryEventData>,
public readonly envelopeId: string,
public readonly envelopeTimestamp: number,
confirm: ConfirmCallback
) {
super('delivery', confirm);
@ -245,9 +248,7 @@ export class MessageEvent extends ConfirmableEvent {
}
export type ReadOrViewEventData = Readonly<{
envelopeId: string;
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceServiceId?: ServiceIdString;
sourceDevice?: number;
@ -256,7 +257,9 @@ export type ReadOrViewEventData = Readonly<{
export class ReadEvent extends ConfirmableEvent {
constructor(
public readonly receipt: ReadOrViewEventData,
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
public readonly envelopeId: string,
public readonly envelopeTimestamp: number,
confirm: ConfirmCallback
) {
super('read', confirm);
@ -265,7 +268,9 @@ export class ReadEvent extends ConfirmableEvent {
export class ViewEvent extends ConfirmableEvent {
constructor(
public readonly receipt: ReadOrViewEventData,
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
public readonly envelopeId: string,
public readonly envelopeTimestamp: number,
confirm: ConfirmCallback
) {
super('view', confirm);
@ -405,7 +410,9 @@ export type ReadSyncEventData = Readonly<{
export class ReadSyncEvent extends ConfirmableEvent {
constructor(
public readonly read: ReadSyncEventData,
public readonly reads: ReadonlyArray<ReadSyncEventData>,
public readonly envelopeId: string,
public readonly envelopeTimestamp: number,
confirm: ConfirmCallback
) {
super('readSync', confirm);
@ -413,16 +420,16 @@ export class ReadSyncEvent extends ConfirmableEvent {
}
export type ViewSyncEventData = Readonly<{
envelopeId: string;
timestamp?: number;
envelopeTimestamp: number;
senderE164?: string;
senderAci?: AciString;
}>;
export class ViewSyncEvent extends ConfirmableEvent {
constructor(
public readonly view: ViewSyncEventData,
public readonly views: ReadonlyArray<ViewSyncEventData>,
public readonly envelopeId: string,
public readonly envelopeTimestamp: number,
confirm: ConfirmCallback
) {
super('viewSync', confirm);
@ -470,15 +477,16 @@ const messageToDeleteSchema = z.union([
authorE164: z.string(),
sentAt: z.number(),
}),
z.object({
type: z.literal('pni').readonly(),
authorPni: z.string().refine(isPniString),
sentAt: z.number(),
}),
]);
export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
const conversationToDeleteSchema = z.union([
z.object({
type: z.literal('group').readonly(),
groupId: z.string(),
}),
z.object({
type: z.literal('aci').readonly(),
aci: z.string().refine(isAciString),
@ -487,6 +495,14 @@ const conversationToDeleteSchema = z.union([
type: z.literal('e164').readonly(),
e164: z.string(),
}),
z.object({
type: z.literal('group').readonly(),
groupId: z.string(),
}),
z.object({
type: z.literal('pni').readonly(),
pni: z.string().refine(isPniString),
}),
]);
export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;

View file

@ -280,7 +280,7 @@ function shouldSyncStatus(callStatus: CallStatus) {
return statusToProto[callStatus] != null;
}
function getProtoForCallHistory(
export function getProtoForCallHistory(
callHistory: CallHistoryDetails
): Proto.SyncMessage.ICallEvent | null {
const event = statusToProto[callHistory.status];
@ -1026,7 +1026,10 @@ async function saveCallHistory({
if (isDeleted) {
if (prevMessage != null) {
await window.Signal.Data.removeMessage(prevMessage.id);
await window.Signal.Data.removeMessage(prevMessage.id, {
fromSync: true,
singleProtoJobQueue,
});
}
return callHistory;
}
@ -1209,32 +1212,10 @@ export async function clearCallHistoryDataAndSync(): Promise<void> {
window.MessageCache.__DEPRECATED$unregister(messageId);
});
const ourAci = window.textsecure.storage.user.getCheckedAci();
const callLogEvent = new Proto.SyncMessage.CallLogEvent({
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
timestamp: Long.fromNumber(timestamp),
});
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callLogEvent = callLogEvent;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
log.info('clearCallHistory: Queueing sync message');
await singleProtoJobQueue.add({
contentHint: ContentHint.RESENDABLE,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
),
type: 'callLogEventSync',
urgent: false,
});
await singleProtoJobQueue.add(
MessageSender.getClearCallHistoryMessage(timestamp)
);
} catch (error) {
log.error('clearCallHistory: Failed to clear call history', error);
}

View file

@ -1,17 +1,67 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import PQueue from 'p-queue';
import { batch } from 'react-redux';
import type { MessageAttributesType } from '../model-types.d';
import { deletePackReference } from '../types/Stickers';
import { isStory } from '../messages/helpers';
import { isDirectConversation } from './whatTypeOfConversation';
import * as log from '../logging/log';
import { getCallHistorySelector } from '../state/selectors/callHistory';
import {
DirectCallStatus,
GroupCallStatus,
AdhocCallStatus,
} from '../types/CallDisposition';
import { getMessageIdForLogging } from './idForLogging';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import { MINUTE } from './durations';
import { drop } from './drop';
export async function cleanupMessage(
message: MessageAttributesType
export async function cleanupMessages(
messages: ReadonlyArray<MessageAttributesType>,
{
fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
}: {
fromSync?: boolean;
markCallHistoryDeleted: (callId: string) => Promise<void>;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
cleanupMessageFromMemory(message);
await deleteMessageData(message);
// First, handle any calls that need to be deleted
const inMemoryQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
drop(
inMemoryQueue.addAll(
messages.map((message: MessageAttributesType) => async () => {
await maybeDeleteCall(message, {
fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
});
})
)
);
await inMemoryQueue.onIdle();
// Then, remove messages from memory, so we can batch the updates in redux
batch(() => {
messages.forEach(message => cleanupMessageFromMemory(message));
});
// Then, handle any asynchronous actions (e.g. deleting data from disk)
const unloadedQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
drop(
unloadedQueue.addAll(
messages.map((message: MessageAttributesType) => async () => {
await deleteMessageData(message);
})
)
);
await unloadedQueue.onIdle();
}
/** Removes a message from redux caches & backbone, but does NOT delete files on disk,
@ -122,3 +172,50 @@ export async function deleteMessageData(
await deletePackReference(message.id, packId);
}
}
export async function maybeDeleteCall(
message: MessageAttributesType,
{
fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
}: {
fromSync?: boolean;
markCallHistoryDeleted: (callId: string) => Promise<void>;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
const { callId } = message;
const logId = `maybeDeleteCall(${getMessageIdForLogging(message)})`;
if (!callId) {
return;
}
const callHistory = getCallHistorySelector(window.reduxStore.getState())(
callId
);
if (!callHistory) {
return;
}
if (
callHistory.status === DirectCallStatus.Pending ||
callHistory.status === GroupCallStatus.Joined ||
callHistory.status === GroupCallStatus.OutgoingRing ||
callHistory.status === GroupCallStatus.Ringing ||
callHistory.status === AdhocCallStatus.Pending
) {
log.warn(
`${logId}: Call status is ${callHistory.status}; not deleting from Call Tab`
);
return;
}
if (!fromSync) {
await singleProtoJobQueue.add(
window.textsecure.MessageSender.getDeleteCallEvent(callHistory)
);
}
await markCallHistoryDeleted(callId);
window.reduxActions.callHistory.removeCallHistory(callId);
}

View file

@ -24,7 +24,9 @@ import type {
ConversationToDelete,
MessageToDelete,
} from '../textsecure/messageReceiverEvents';
import type { AciString } from '../types/ServiceId';
import { isPniString } from '../types/ServiceId';
import type { AciString, PniString } from '../types/ServiceId';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
const {
getMessagesBySentAt,
@ -48,12 +50,16 @@ export function doesMessageMatch({
const conversationMatches = message.conversationId === conversationId;
const aciMatches =
query.authorAci && author?.attributes.serviceId === query.authorAci;
const pniMatches =
query.authorPni && author?.attributes.serviceId === query.authorPni;
const e164Matches =
query.authorE164 && author?.attributes.e164 === query.authorE164;
const timestampMatches = sentTimestamps.has(query.sentAt);
return Boolean(
conversationMatches && timestampMatches && (aciMatches || e164Matches)
conversationMatches &&
timestampMatches &&
(aciMatches || e164Matches || pniMatches)
);
}
@ -91,7 +97,10 @@ export async function deleteMessage(
return false;
}
await deleteAndCleanup([found], logId);
await deleteAndCleanup([found], logId, {
fromSync: true,
singleProtoJobQueue,
});
return true;
}
@ -113,8 +122,10 @@ export async function deleteConversation(
const { received_at: receivedAt } = newestMessage;
await removeMessagesInConversation(conversation.id, {
fromSync: true,
receivedAt,
logId: `${logId}(receivedAt=${receivedAt})`,
singleProtoJobQueue,
});
}
@ -170,6 +181,9 @@ export function getConversationFromTarget(
if (type === 'e164') {
return window.ConversationController.get(targetConversation.e164);
}
if (type === 'pni') {
return window.ConversationController.get(targetConversation.pni);
}
throw missingCaseError(type);
}
@ -178,6 +192,7 @@ type MessageQuery = {
sentAt: number;
authorAci?: AciString;
authorE164?: string;
authorPni?: PniString;
};
export function getMessageQueryFromTarget(
@ -191,6 +206,13 @@ export function getMessageQueryFromTarget(
}
return { sentAt, authorAci: targetMessage.authorAci };
}
if (type === 'pni') {
if (!isPniString(targetMessage.authorPni)) {
throw new Error('Provided authorPni was not a PNI!');
}
return { sentAt, authorPni: targetMessage.authorPni };
}
if (type === 'e164') {
return { sentAt, authorE164: targetMessage.authorE164 };
}

View file

@ -4,6 +4,7 @@
import * as log from '../logging/log';
import { calculateExpirationTimestamp } from './expirationTimer';
import { DAY } from './durations';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
const existingOnboardingStoryMessageIds = window.storage.get(
@ -43,7 +44,9 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
await window.Signal.Data.removeMessages(existingOnboardingStoryMessageIds);
await window.Signal.Data.removeMessages(existingOnboardingStoryMessageIds, {
singleProtoJobQueue,
});
await window.storage.put('existingOnboardingStoryMessageIds', undefined);

View file

@ -217,6 +217,7 @@ export function getConversation(model: ConversationModel): ConversationType {
profileName: getProfileName(attributes),
profileSharing: attributes.profileSharing,
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
capabilities: attributes.capabilities,
sharingPhoneNumber: attributes.sharingPhoneNumber,
publicParams: attributes.publicParams,
secretParams: attributes.secretParams,

View file

@ -7,7 +7,7 @@ import type { ConversationAttributesType } from '../model-types.d';
import { hasErrors } from '../state/selectors/message';
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
import { notificationService } from '../services/notifications';
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
import * as log from '../logging/log';
@ -196,7 +196,7 @@ export async function markConversationRead(
}
}
void expiringMessagesDeletionService.update();
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
return true;

View file

@ -29,6 +29,7 @@ import { getSourceServiceId } from '../messages/helpers';
import { missingCaseError } from './missingCaseError';
import { reduce } from './iterables';
import { strictAssert } from './assert';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
export enum ModifyTargetMessageResult {
Modified = 'Modified',
@ -55,24 +56,28 @@ export async function modifyTargetMessage(
const syncDeletes = await DeletesForMe.forMessage(message.attributes);
if (syncDeletes.length) {
if (!isFirstRun) {
await window.Signal.Data.removeMessage(message.id);
await window.Signal.Data.removeMessage(message.id, {
fromSync: true,
singleProtoJobQueue,
});
}
return ModifyTargetMessageResult.Deleted;
}
if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
const sendActions = MessageReceipts.forMessage(message).map(receipt => {
const receipts = await MessageReceipts.forMessage(message);
const sendActions = receipts.map(({ receiptSync }) => {
let sendActionType: SendActionType;
const receiptType = receipt.type;
const receiptType = receiptSync.type;
switch (receiptType) {
case MessageReceipts.MessageReceiptType.Delivery:
case MessageReceipts.messageReceiptTypeSchema.enum.Delivery:
sendActionType = SendActionType.GotDeliveryReceipt;
break;
case MessageReceipts.MessageReceiptType.Read:
case MessageReceipts.messageReceiptTypeSchema.enum.Read:
sendActionType = SendActionType.GotReadReceipt;
break;
case MessageReceipts.MessageReceiptType.View:
case MessageReceipts.messageReceiptTypeSchema.enum.View:
sendActionType = SendActionType.GotViewedReceipt;
break;
default:
@ -80,10 +85,10 @@ export async function modifyTargetMessage(
}
return {
destinationConversationId: receipt.sourceConversationId,
destinationConversationId: receiptSync.sourceConversationId,
action: {
type: sendActionType,
updatedAt: receipt.receiptTimestamp,
updatedAt: receiptSync.receiptTimestamp,
},
};
});
@ -123,10 +128,10 @@ export async function modifyTargetMessage(
if (type === 'incoming') {
// In a followup (see DESKTOP-2100), we want to make `ReadSyncs#forMessage` return
// an array, not an object. This array wrapping makes that future a bit easier.
const readSync = ReadSyncs.forMessage(message);
const readSyncs = readSync ? [readSync] : [];
const maybeSingleReadSync = await ReadSyncs.forMessage(message);
const readSyncs = maybeSingleReadSync ? [maybeSingleReadSync] : [];
const viewSyncs = ViewSyncs.forMessage(message);
const viewSyncs = await ViewSyncs.forMessage(message);
const isGroupStoryReply =
isGroup(conversation.attributes) && message.get('storyId');
@ -134,8 +139,8 @@ export async function modifyTargetMessage(
if (readSyncs.length !== 0 || viewSyncs.length !== 0) {
const markReadAt = Math.min(
Date.now(),
...readSyncs.map(sync => sync.readAt),
...viewSyncs.map(sync => sync.viewedAt)
...readSyncs.map(({ readSync }) => readSync.readAt),
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
);
if (message.get('expireTimer')) {
@ -180,7 +185,7 @@ export async function modifyTargetMessage(
if (!isFirstRun && message.getPendingMarkRead()) {
const markReadAt = message.getPendingMarkRead();
message.setPendingMarkRead(undefined);
const newestSentAt = readSync?.timestamp;
const newestSentAt = maybeSingleReadSync?.readSync.timestamp;
// This is primarily to allow the conversation to mark all older
// messages as read, as is done when we receive a read sync for
@ -207,7 +212,7 @@ export async function modifyTargetMessage(
}
if (isStory(message.attributes)) {
const viewSyncs = ViewSyncs.forMessage(message);
const viewSyncs = await ViewSyncs.forMessage(message);
if (viewSyncs.length !== 0) {
message.set({
@ -218,7 +223,7 @@ export async function modifyTargetMessage(
const markReadAt = Math.min(
Date.now(),
...viewSyncs.map(sync => sync.viewedAt)
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
);
message.setPendingMarkRead(
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)

View file

@ -4,6 +4,7 @@
import { z } from 'zod';
import type { ZodSchema } from 'zod';
import { drop } from './drop';
import * as log from '../logging/log';
import * as DeletesForMe from '../messageModifiers/DeletesForMe';
import {
@ -11,18 +12,31 @@ import {
deleteConversationSchema,
deleteLocalConversationSchema,
} from '../textsecure/messageReceiverEvents';
import {
receiptSyncTaskSchema,
onReceipt,
} from '../messageModifiers/MessageReceipts';
import {
deleteConversation,
deleteLocalOnlyConversation,
getConversationFromTarget,
} from './deleteForMe';
import { drop } from './drop';
import {
onSync as onReadSync,
readSyncTaskSchema,
} from '../messageModifiers/ReadSyncs';
import {
onSync as onViewSync,
viewSyncTaskSchema,
} from '../messageModifiers/ViewSyncs';
const syncTaskDataSchema = z.union([
deleteMessageSchema,
deleteConversationSchema,
deleteLocalConversationSchema,
receiptSyncTaskSchema,
readSyncTaskSchema,
viewSyncTaskSchema,
]);
export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
@ -40,6 +54,11 @@ const SCHEMAS_BY_TYPE: Record<SyncTaskData['type'], ZodSchema> = {
'delete-message': deleteMessageSchema,
'delete-conversation': deleteConversationSchema,
'delete-local-conversation': deleteLocalConversationSchema,
Delivery: receiptSyncTaskSchema,
Read: receiptSyncTaskSchema,
View: receiptSyncTaskSchema,
ReadSync: readSyncTaskSchema,
ViewSync: viewSyncTaskSchema,
};
function toLogId(task: SyncTaskType) {
@ -77,14 +96,15 @@ export async function queueSyncTasks(
const { data: parsed } = parseResult;
if (parsed.type === 'delete-message') {
// eslint-disable-next-line no-await-in-loop
await DeletesForMe.onDelete({
conversation: parsed.conversation,
envelopeId,
message: parsed.message,
syncTaskId: id,
timestamp: sentAt,
});
drop(
DeletesForMe.onDelete({
conversation: parsed.conversation,
envelopeId,
message: parsed.message,
syncTaskId: id,
timestamp: sentAt,
})
);
} else if (parsed.type === 'delete-conversation') {
const {
conversation: targetConversation,
@ -133,6 +153,39 @@ export async function queueSyncTasks(
log.info(`${logId}: Done; result=${result}`);
})
);
} else if (
parsed.type === 'Delivery' ||
parsed.type === 'Read' ||
parsed.type === 'View'
) {
drop(
onReceipt({
envelopeId,
receiptSync: parsed,
syncTaskId: id,
})
);
} else if (parsed.type === 'ReadSync') {
drop(
onReadSync({
envelopeId,
readSync: parsed,
syncTaskId: id,
})
);
} else if (parsed.type === 'ViewSync') {
drop(
onViewSync({
envelopeId,
viewSync: parsed,
syncTaskId: id,
})
);
} else {
const parsedType: never = parsed.type;
log.error(`${logId}: Encountered job of type ${parsedType}, removing`);
// eslint-disable-next-line no-await-in-loop
await removeSyncTaskById(id);
}
}
}