Move receipts and view/read syncs to new syncTasks system
This commit is contained in:
parent
1a263e63da
commit
75c32e86f0
33 changed files with 1242 additions and 612 deletions
|
@ -636,7 +636,7 @@ message SyncMessage {
|
||||||
message DeleteForMe {
|
message DeleteForMe {
|
||||||
message ConversationIdentifier {
|
message ConversationIdentifier {
|
||||||
oneof identifier {
|
oneof identifier {
|
||||||
string threadAci = 1;
|
string threadServiceId = 1;
|
||||||
bytes threadGroupId = 2;
|
bytes threadGroupId = 2;
|
||||||
string threadE164 = 3;
|
string threadE164 = 3;
|
||||||
}
|
}
|
||||||
|
@ -644,7 +644,7 @@ message SyncMessage {
|
||||||
|
|
||||||
message AddressableMessage {
|
message AddressableMessage {
|
||||||
oneof author {
|
oneof author {
|
||||||
string authorAci = 1;
|
string authorServiceId = 1;
|
||||||
string authorE164 = 2;
|
string authorE164 = 2;
|
||||||
}
|
}
|
||||||
optional uint64 sentTimestamp = 3;
|
optional uint64 sentTimestamp = 3;
|
||||||
|
|
275
ts/background.ts
275
ts/background.ts
|
@ -42,7 +42,10 @@ import { isNotNil } from './util/isNotNil';
|
||||||
import { isBackupEnabled } from './util/isBackupEnabled';
|
import { isBackupEnabled } from './util/isBackupEnabled';
|
||||||
import { setAppLoadingScreenMessage } from './setAppLoadingScreenMessage';
|
import { setAppLoadingScreenMessage } from './setAppLoadingScreenMessage';
|
||||||
import { IdleDetector } from './IdleDetector';
|
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 { tapToViewMessagesDeletionService } from './services/tapToViewMessagesDeletionService';
|
||||||
import { getStoriesForRedux, loadStories } from './services/storyLoader';
|
import { getStoriesForRedux, loadStories } from './services/storyLoader';
|
||||||
import {
|
import {
|
||||||
|
@ -116,17 +119,12 @@ import * as Edits from './messageModifiers/Edits';
|
||||||
import * as MessageReceipts from './messageModifiers/MessageReceipts';
|
import * as MessageReceipts from './messageModifiers/MessageReceipts';
|
||||||
import * as MessageRequests from './messageModifiers/MessageRequests';
|
import * as MessageRequests from './messageModifiers/MessageRequests';
|
||||||
import * as Reactions from './messageModifiers/Reactions';
|
import * as Reactions from './messageModifiers/Reactions';
|
||||||
import * as ReadSyncs from './messageModifiers/ReadSyncs';
|
|
||||||
import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
|
import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
|
||||||
import * as ViewSyncs from './messageModifiers/ViewSyncs';
|
|
||||||
import type { DeleteAttributesType } from './messageModifiers/Deletes';
|
import type { DeleteAttributesType } from './messageModifiers/Deletes';
|
||||||
import type { EditAttributesType } from './messageModifiers/Edits';
|
import type { EditAttributesType } from './messageModifiers/Edits';
|
||||||
import type { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
|
|
||||||
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
|
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
|
||||||
import type { ReactionAttributesType } from './messageModifiers/Reactions';
|
import type { ReactionAttributesType } from './messageModifiers/Reactions';
|
||||||
import type { ReadSyncAttributesType } from './messageModifiers/ReadSyncs';
|
|
||||||
import type { ViewOnceOpenSyncAttributesType } from './messageModifiers/ViewOnceOpenSyncs';
|
import type { ViewOnceOpenSyncAttributesType } from './messageModifiers/ViewOnceOpenSyncs';
|
||||||
import type { ViewSyncAttributesType } from './messageModifiers/ViewSyncs';
|
|
||||||
import { ReadStatus } from './messages/MessageReadStatus';
|
import { ReadStatus } from './messages/MessageReadStatus';
|
||||||
import type { SendStateByConversationId } from './messages/MessageSendState';
|
import type { SendStateByConversationId } from './messages/MessageSendState';
|
||||||
import { SendStatus } from './messages/MessageSendState';
|
import { SendStatus } from './messages/MessageSendState';
|
||||||
|
@ -202,7 +200,11 @@ import { getThemeType } from './util/getThemeType';
|
||||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||||
import { CallMode } from './types/Calling';
|
import { CallMode } from './types/Calling';
|
||||||
|
import type { SyncTaskType } from './util/syncTasks';
|
||||||
import { queueSyncTasks } 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 { isEnabled } from './RemoteConfig';
|
||||||
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
|
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
|
||||||
import { getConversationIdForLogging } from './util/idForLogging';
|
import { getConversationIdForLogging } from './util/idForLogging';
|
||||||
|
@ -1498,10 +1500,12 @@ export async function startApp(): Promise<void> {
|
||||||
window.Whisper.events.trigger('timetravel');
|
window.Whisper.events.trigger('timetravel');
|
||||||
});
|
});
|
||||||
|
|
||||||
void expiringMessagesDeletionService.update();
|
initializeExpiringMessageService(singleProtoJobQueue);
|
||||||
|
|
||||||
|
void updateExpiringMessagesService();
|
||||||
void tapToViewMessagesDeletionService.update();
|
void tapToViewMessagesDeletionService.update();
|
||||||
window.Whisper.events.on('timetravel', () => {
|
window.Whisper.events.on('timetravel', () => {
|
||||||
void expiringMessagesDeletionService.update();
|
void updateExpiringMessagesService();
|
||||||
void tapToViewMessagesDeletionService.update();
|
void tapToViewMessagesDeletionService.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1833,7 +1837,9 @@ export async function startApp(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Note: we always have to register our capabilities all at once, so we do this
|
// Note: we always have to register our capabilities all at once, so we do this
|
||||||
// after connect on every startup
|
// after connect on every startup
|
||||||
await server.registerCapabilities({});
|
await server.registerCapabilities({
|
||||||
|
deleteSync: true,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
'Error: Unable to register our capabilities.',
|
'Error: Unable to register our capabilities.',
|
||||||
|
@ -3221,47 +3227,51 @@ export async function startApp(): Promise<void> {
|
||||||
drop(MessageRequests.onResponse(attributes));
|
drop(MessageRequests.onResponse(attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadReceipt(event: Readonly<ReadEvent>): void {
|
async function onReadReceipt(event: Readonly<ReadEvent>): Promise<void> {
|
||||||
onReadOrViewReceipt({
|
return onReadOrViewReceipt({
|
||||||
logTitle: 'read receipt',
|
logTitle: 'read receipt',
|
||||||
event,
|
event,
|
||||||
type: MessageReceipts.MessageReceiptType.Read,
|
type: MessageReceipts.messageReceiptTypeSchema.enum.Read,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewReceipt(event: Readonly<ViewEvent>): void {
|
async function onViewReceipt(event: Readonly<ViewEvent>): Promise<void> {
|
||||||
onReadOrViewReceipt({
|
return onReadOrViewReceipt({
|
||||||
logTitle: 'view receipt',
|
logTitle: 'view receipt',
|
||||||
event,
|
event,
|
||||||
type: MessageReceipts.MessageReceiptType.View,
|
type: MessageReceipts.messageReceiptTypeSchema.enum.View,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadOrViewReceipt({
|
async function onReadOrViewReceipt({
|
||||||
event,
|
event,
|
||||||
logTitle,
|
logTitle,
|
||||||
type,
|
type,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
event: ReadEvent | ViewEvent;
|
event: ReadEvent | ViewEvent;
|
||||||
logTitle: string;
|
logTitle: string;
|
||||||
type:
|
type: 'Read' | 'View';
|
||||||
| MessageReceipts.MessageReceiptType.Read
|
}>): Promise<void> {
|
||||||
| MessageReceipts.MessageReceiptType.View;
|
const { receipts, envelopeId, envelopeTimestamp, confirm } = event;
|
||||||
}>): void {
|
const logId = `onReadOrViewReceipt(type=${type}, envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
|
||||||
|
|
||||||
|
const syncTasks = receipts
|
||||||
|
.map((receipt): SyncTaskType | undefined => {
|
||||||
const {
|
const {
|
||||||
envelopeTimestamp,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
source,
|
source,
|
||||||
sourceServiceId,
|
sourceServiceId,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
} = event.receipt;
|
} = receipt;
|
||||||
const sourceConversation = window.ConversationController.lookupOrCreate({
|
const sourceConversation = window.ConversationController.lookupOrCreate(
|
||||||
|
{
|
||||||
serviceId: sourceServiceId,
|
serviceId: sourceServiceId,
|
||||||
e164: source,
|
e164: source,
|
||||||
reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
|
reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
|
||||||
});
|
}
|
||||||
strictAssert(sourceConversation, 'Failed to create conversation');
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
logTitle,
|
logTitle,
|
||||||
`${sourceServiceId || source}.${sourceDevice}`,
|
`${sourceServiceId || source}.${sourceDevice}`,
|
||||||
|
@ -3270,15 +3280,20 @@ export async function startApp(): Promise<void> {
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(
|
if (!sourceConversation) {
|
||||||
isServiceIdString(sourceServiceId),
|
log.error(`${logId}: Failed to create conversation`);
|
||||||
'onReadOrViewReceipt: Missing sourceServiceId'
|
return undefined;
|
||||||
);
|
}
|
||||||
strictAssert(sourceDevice, 'onReadOrViewReceipt: Missing sourceDevice');
|
if (!isServiceIdString(sourceServiceId)) {
|
||||||
|
log.error(`${logId}: Missing sourceServiceId`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!sourceDevice) {
|
||||||
|
log.error(`${logId}: Missing sourceDevice`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const attributes: MessageReceiptAttributesType = {
|
const data: ReceiptSyncTaskType = {
|
||||||
envelopeId: event.receipt.envelopeId,
|
|
||||||
removeFromMessageReceiverCache: event.confirm,
|
|
||||||
messageSentAt: timestamp,
|
messageSentAt: timestamp,
|
||||||
receiptTimestamp: envelopeTimestamp,
|
receiptTimestamp: envelopeTimestamp,
|
||||||
sourceConversationId: sourceConversation.id,
|
sourceConversationId: sourceConversation.id,
|
||||||
|
@ -3287,11 +3302,38 @@ export async function startApp(): Promise<void> {
|
||||||
type,
|
type,
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
};
|
};
|
||||||
drop(MessageReceipts.onReceipt(attributes));
|
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> {
|
async function onReadSync(ev: ReadSyncEvent): Promise<void> {
|
||||||
const { envelopeTimestamp, sender, senderAci, timestamp } = ev.read;
|
const { reads, envelopeTimestamp, envelopeId, confirm } = ev;
|
||||||
|
const logId = `onReadSync(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
|
||||||
|
|
||||||
|
const syncTasks = reads
|
||||||
|
.map((read): SyncTaskType | undefined => {
|
||||||
|
const { sender, senderAci, timestamp } = read;
|
||||||
const readAt = envelopeTimestamp;
|
const readAt = envelopeTimestamp;
|
||||||
const { conversation: senderConversation } =
|
const { conversation: senderConversation } =
|
||||||
window.ConversationController.maybeMergeContacts({
|
window.ConversationController.maybeMergeContacts({
|
||||||
|
@ -3311,25 +3353,60 @@ export async function startApp(): Promise<void> {
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(senderId, 'onReadSync missing senderId');
|
if (!senderId) {
|
||||||
strictAssert(senderAci, 'onReadSync missing senderAci');
|
log.error(`${logId}: missing senderId`);
|
||||||
strictAssert(timestamp, 'onReadSync missing timestamp');
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!senderAci) {
|
||||||
|
log.error(`${logId}: missing senderAci`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!timestamp) {
|
||||||
|
log.error(`${logId}: missing timestamp`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const attributes: ReadSyncAttributesType = {
|
const data: ReadSyncTaskType = {
|
||||||
envelopeId: ev.read.envelopeId,
|
type: 'ReadSync',
|
||||||
removeFromMessageReceiverCache: ev.confirm,
|
|
||||||
senderId,
|
senderId,
|
||||||
sender,
|
sender,
|
||||||
senderAci,
|
senderAci,
|
||||||
timestamp,
|
timestamp,
|
||||||
readAt,
|
readAt,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
id: generateUuid(),
|
||||||
|
attempts: 1,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data,
|
||||||
|
envelopeId,
|
||||||
|
sentAt: envelopeTimestamp,
|
||||||
|
type: 'ReadSync',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
await ReadSyncs.onSync(attributes);
|
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> {
|
async function onViewSync(ev: ViewSyncEvent): Promise<void> {
|
||||||
const { envelopeTimestamp, senderE164, senderAci, timestamp } = ev.view;
|
const { envelopeTimestamp, envelopeId, views, confirm } = ev;
|
||||||
|
const logId = `onViewSync=(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
|
||||||
|
|
||||||
|
const syncTasks = views
|
||||||
|
.map((view): SyncTaskType | undefined => {
|
||||||
|
const { senderAci, senderE164, timestamp } = view;
|
||||||
|
|
||||||
const { conversation: senderConversation } =
|
const { conversation: senderConversation } =
|
||||||
window.ConversationController.maybeMergeContacts({
|
window.ConversationController.maybeMergeContacts({
|
||||||
e164: senderE164,
|
e164: senderE164,
|
||||||
|
@ -3348,27 +3425,62 @@ export async function startApp(): Promise<void> {
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(senderId, 'onViewSync missing senderId');
|
if (!senderId) {
|
||||||
strictAssert(senderAci, 'onViewSync missing senderAci');
|
log.error(`${logId}: missing senderId`);
|
||||||
strictAssert(timestamp, 'onViewSync missing timestamp');
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!senderAci) {
|
||||||
|
log.error(`${logId}: missing senderAci`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!timestamp) {
|
||||||
|
log.error(`${logId}: missing timestamp`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const attributes: ViewSyncAttributesType = {
|
const data: ViewSyncTaskType = {
|
||||||
envelopeId: ev.view.envelopeId,
|
type: 'ViewSync',
|
||||||
removeFromMessageReceiverCache: ev.confirm,
|
|
||||||
senderId,
|
senderId,
|
||||||
senderE164,
|
senderE164,
|
||||||
senderAci,
|
senderAci,
|
||||||
timestamp,
|
timestamp,
|
||||||
viewedAt: envelopeTimestamp,
|
viewedAt: envelopeTimestamp,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
id: generateUuid(),
|
||||||
|
attempts: 1,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data,
|
||||||
|
envelopeId,
|
||||||
|
sentAt: envelopeTimestamp,
|
||||||
|
type: 'ViewSync',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
await ViewSyncs.onSync(attributes);
|
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 {
|
async function onDeliveryReceipt(ev: DeliveryEvent): Promise<void> {
|
||||||
const { deliveryReceipt } = ev;
|
const { deliveryReceipts, envelopeId, envelopeTimestamp, confirm } = ev;
|
||||||
|
const logId = `onDeliveryReceipt(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
|
||||||
|
|
||||||
|
strictAssert(envelopeTimestamp, `${logId}: missing envelopeTimestamp`);
|
||||||
|
strictAssert(envelopeTimestamp, `${logId}: missing envelopeId`);
|
||||||
|
|
||||||
|
const syncTasks = deliveryReceipts
|
||||||
|
.map((deliveryReceipt): SyncTaskType | undefined => {
|
||||||
const {
|
const {
|
||||||
envelopeTimestamp,
|
|
||||||
sourceServiceId,
|
sourceServiceId,
|
||||||
source,
|
source,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
|
@ -3376,11 +3488,13 @@ export async function startApp(): Promise<void> {
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
} = deliveryReceipt;
|
} = deliveryReceipt;
|
||||||
|
|
||||||
const sourceConversation = window.ConversationController.lookupOrCreate({
|
const sourceConversation = window.ConversationController.lookupOrCreate(
|
||||||
|
{
|
||||||
serviceId: sourceServiceId,
|
serviceId: sourceServiceId,
|
||||||
e164: source,
|
e164: source,
|
||||||
reason: `onDeliveryReceipt(${envelopeTimestamp})`,
|
reason: `onDeliveryReceipt(${envelopeTimestamp})`,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'delivery receipt from',
|
'delivery receipt from',
|
||||||
|
@ -3391,30 +3505,51 @@ export async function startApp(): Promise<void> {
|
||||||
`wasSentEncrypted=${wasSentEncrypted}`
|
`wasSentEncrypted=${wasSentEncrypted}`
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(
|
if (!isServiceIdString(sourceServiceId)) {
|
||||||
envelopeTimestamp,
|
log.error(`${logId}: missing valid sourceServiceId`);
|
||||||
'onDeliveryReceipt: missing envelopeTimestamp'
|
return undefined;
|
||||||
);
|
}
|
||||||
strictAssert(
|
if (!sourceDevice) {
|
||||||
isServiceIdString(sourceServiceId),
|
log.error(`${logId}: missing sourceDevice`);
|
||||||
'onDeliveryReceipt: missing valid sourceServiceId'
|
return undefined;
|
||||||
);
|
}
|
||||||
strictAssert(sourceDevice, 'onDeliveryReceipt: missing sourceDevice');
|
if (!sourceConversation) {
|
||||||
strictAssert(sourceConversation, 'onDeliveryReceipt: missing conversation');
|
log.error(`${logId}: missing conversation`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const attributes: MessageReceiptAttributesType = {
|
const data: ReceiptSyncTaskType = {
|
||||||
envelopeId: ev.deliveryReceipt.envelopeId,
|
|
||||||
removeFromMessageReceiverCache: ev.confirm,
|
|
||||||
messageSentAt: timestamp,
|
messageSentAt: timestamp,
|
||||||
receiptTimestamp: envelopeTimestamp,
|
receiptTimestamp: envelopeTimestamp,
|
||||||
sourceConversationId: sourceConversation.id,
|
sourceConversationId: sourceConversation.id,
|
||||||
sourceServiceId,
|
sourceServiceId,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
type: MessageReceipts.MessageReceiptType.Delivery,
|
type: MessageReceipts.messageReceiptTypeSchema.enum.Delivery,
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
id: generateUuid(),
|
||||||
|
attempts: 1,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data,
|
||||||
|
envelopeId,
|
||||||
|
sentAt: envelopeTimestamp,
|
||||||
|
type: 'Delivery',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
drop(MessageReceipts.onReceipt(attributes));
|
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) {
|
async function onDeleteForMeSync(ev: DeleteForMeSyncEvent) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2016 Signal Messenger, LLC
|
// Copyright 2016 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
|
@ -10,7 +11,7 @@ import { isOutgoing, isStory } from '../state/selectors/message';
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { createWaitBatcher } from '../util/waitBatcher';
|
import { createWaitBatcher } from '../util/waitBatcher';
|
||||||
import type { ServiceIdString } from '../types/ServiceId';
|
import { isServiceIdString } from '../types/ServiceId';
|
||||||
import {
|
import {
|
||||||
SendActionType,
|
SendActionType,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
|
@ -23,7 +24,6 @@ import * as log from '../logging/log';
|
||||||
import { getSourceServiceId } from '../messages/helpers';
|
import { getSourceServiceId } from '../messages/helpers';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
import { generateCacheKey } from './generateCacheKey';
|
|
||||||
import { getPropForTimestamp } from '../util/editHelpers';
|
import { getPropForTimestamp } from '../util/editHelpers';
|
||||||
import {
|
import {
|
||||||
DELETE_SENT_PROTO_BATCHER_WAIT_MS,
|
DELETE_SENT_PROTO_BATCHER_WAIT_MS,
|
||||||
|
@ -31,34 +31,30 @@ import {
|
||||||
} from '../types/Receipt';
|
} from '../types/Receipt';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
const { deleteSentProtoRecipient } = dataInterface;
|
const { deleteSentProtoRecipient, removeSyncTaskById } = dataInterface;
|
||||||
|
|
||||||
export enum MessageReceiptType {
|
export const messageReceiptTypeSchema = z.enum(['Delivery', 'Read', 'View']);
|
||||||
Delivery = 'Delivery',
|
|
||||||
Read = 'Read',
|
export type MessageReceiptType = z.infer<typeof messageReceiptTypeSchema>;
|
||||||
View = 'View',
|
|
||||||
}
|
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 = {
|
export type MessageReceiptAttributesType = {
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
messageSentAt: number;
|
syncTaskId: string;
|
||||||
receiptTimestamp: number;
|
receiptSync: ReceiptSyncTaskType;
|
||||||
removeFromMessageReceiverCache: () => void;
|
|
||||||
sourceConversationId: string;
|
|
||||||
sourceDevice: number;
|
|
||||||
sourceServiceId: ServiceIdString;
|
|
||||||
type: MessageReceiptType;
|
|
||||||
wasSentEncrypted: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getReceiptCacheKey(receipt: MessageReceiptAttributesType): string {
|
|
||||||
return generateCacheKey({
|
|
||||||
sender: receipt.sourceServiceId,
|
|
||||||
timestamp: receipt.messageSentAt,
|
|
||||||
type: receipt.type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedReceipts = new Map<string, MessageReceiptAttributesType>();
|
const cachedReceipts = new Map<string, MessageReceiptAttributesType>();
|
||||||
|
|
||||||
const processReceiptBatcher = createWaitBatcher({
|
const processReceiptBatcher = createWaitBatcher({
|
||||||
|
@ -69,7 +65,7 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
// First group by sentAt, so that we can find the target message
|
// First group by sentAt, so that we can find the target message
|
||||||
const receiptsByMessageSentAt = groupBy(
|
const receiptsByMessageSentAt = groupBy(
|
||||||
receipts,
|
receipts,
|
||||||
receipt => receipt.messageSentAt
|
receipt => receipt.receiptSync.messageSentAt
|
||||||
);
|
);
|
||||||
|
|
||||||
// Once we find the message, we'll group them by messageId to process
|
// Once we find the message, we'll group them by messageId to process
|
||||||
|
@ -99,7 +95,7 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// All receipts have the same sentAt, so we can grab it from the first
|
// 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 =
|
const messagesMatchingTimestamp =
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
@ -114,14 +110,16 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
for (const receipt of receiptsForMessageSentAt) {
|
for (const receipt of receiptsForMessageSentAt) {
|
||||||
|
const { receiptSync } = receipt;
|
||||||
log.info(
|
log.info(
|
||||||
'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
|
'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
|
||||||
receipt.messageSentAt,
|
receiptSync.messageSentAt,
|
||||||
receipt.type,
|
receiptSync.type,
|
||||||
receipt.sourceConversationId,
|
receiptSync.sourceConversationId,
|
||||||
receipt.sourceServiceId
|
receiptSync.sourceServiceId
|
||||||
);
|
);
|
||||||
remove(receipt);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await remove(receipt);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -129,7 +127,7 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
|
|
||||||
for (const receipt of receiptsForMessageSentAt) {
|
for (const receipt of receiptsForMessageSentAt) {
|
||||||
const targetMessage = getTargetMessage({
|
const targetMessage = getTargetMessage({
|
||||||
sourceConversationId: receipt.sourceConversationId,
|
sourceConversationId: receipt.receiptSync.sourceConversationId,
|
||||||
targetTimestamp: sentAt,
|
targetTimestamp: sentAt,
|
||||||
messagesMatchingTimestamp,
|
messagesMatchingTimestamp,
|
||||||
});
|
});
|
||||||
|
@ -144,7 +142,9 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
item.sendStateByConversationId &&
|
item.sendStateByConversationId &&
|
||||||
!item.deletedForEveryone &&
|
!item.deletedForEveryone &&
|
||||||
Boolean(
|
Boolean(
|
||||||
item.sendStateByConversationId[receipt.sourceConversationId]
|
item.sendStateByConversationId[
|
||||||
|
receipt.receiptSync.sourceConversationId
|
||||||
|
]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -154,12 +154,13 @@ const processReceiptBatcher = createWaitBatcher({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Nope, no target message was found
|
// Nope, no target message was found
|
||||||
|
const { receiptSync } = receipt;
|
||||||
log.info(
|
log.info(
|
||||||
'MessageReceipts.processReceiptBatcher: No message for receipt',
|
'MessageReceipts.processReceiptBatcher: No message for receipt',
|
||||||
receipt.messageSentAt,
|
receiptSync.messageSentAt,
|
||||||
receipt.type,
|
receiptSync.type,
|
||||||
receipt.sourceConversationId,
|
receiptSync.sourceConversationId,
|
||||||
receipt.sourceServiceId
|
receiptSync.sourceServiceId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +191,7 @@ async function processReceiptsForMessage(
|
||||||
messageId
|
messageId
|
||||||
);
|
);
|
||||||
|
|
||||||
const { updatedMessage, validReceipts } = updateMessageWithReceipts(
|
const { updatedMessage, validReceipts } = await updateMessageWithReceipts(
|
||||||
message,
|
message,
|
||||||
receipts
|
receipts
|
||||||
);
|
);
|
||||||
|
@ -204,7 +205,8 @@ async function processReceiptsForMessage(
|
||||||
|
|
||||||
// Confirm/remove receipts, and delete sent protos
|
// Confirm/remove receipts, and delete sent protos
|
||||||
for (const receipt of validReceipts) {
|
for (const receipt of validReceipts) {
|
||||||
remove(receipt);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await remove(receipt);
|
||||||
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
|
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,25 +217,27 @@ async function processReceiptsForMessage(
|
||||||
conversation?.debouncedUpdateLastMessage?.();
|
conversation?.debouncedUpdateLastMessage?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageWithReceipts(
|
async function updateMessageWithReceipts(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
receipts: Array<MessageReceiptAttributesType>
|
receipts: Array<MessageReceiptAttributesType>
|
||||||
): {
|
): Promise<{
|
||||||
updatedMessage: MessageAttributesType;
|
updatedMessage: MessageAttributesType;
|
||||||
validReceipts: Array<MessageReceiptAttributesType>;
|
validReceipts: Array<MessageReceiptAttributesType>;
|
||||||
} {
|
}> {
|
||||||
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
|
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
|
||||||
|
|
||||||
|
const toRemove: Array<MessageReceiptAttributesType> = [];
|
||||||
const receiptsToProcess = receipts.filter(receipt => {
|
const receiptsToProcess = receipts.filter(receipt => {
|
||||||
if (shouldDropReceipt(receipt, message)) {
|
if (shouldDropReceipt(receipt, message)) {
|
||||||
|
const { receiptSync } = receipt;
|
||||||
log.info(
|
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;
|
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!
|
// Between the time it was received and now, this receipt has already been handled!
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -241,6 +245,8 @@ function updateMessageWithReceipts(
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Promise.all(toRemove.map(remove));
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: batch processing ${receipts.length}` +
|
`${logId}: batch processing ${receipts.length}` +
|
||||||
` receipt${receipts.length === 1 ? '' : 's'}`
|
` receipt${receipts.length === 1 ? '' : 's'}`
|
||||||
|
@ -287,9 +293,10 @@ const deleteSentProtoBatcher = createWaitBatcher({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function remove(receipt: MessageReceiptAttributesType): void {
|
async function remove(receipt: MessageReceiptAttributesType): Promise<void> {
|
||||||
cachedReceipts.delete(getReceiptCacheKey(receipt));
|
const { syncTaskId } = receipt;
|
||||||
receipt.removeFromMessageReceiverCache();
|
cachedReceipts.delete(syncTaskId);
|
||||||
|
await removeSyncTaskById(syncTaskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTargetMessage({
|
function getTargetMessage({
|
||||||
|
@ -372,13 +379,13 @@ const shouldDropReceipt = (
|
||||||
receipt: MessageReceiptAttributesType,
|
receipt: MessageReceiptAttributesType,
|
||||||
message: MessageAttributesType
|
message: MessageAttributesType
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const { type } = receipt;
|
const { type } = receipt.receiptSync;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageReceiptType.Delivery:
|
case messageReceiptTypeSchema.Enum.Delivery:
|
||||||
return false;
|
return false;
|
||||||
case MessageReceiptType.Read:
|
case messageReceiptTypeSchema.Enum.Read:
|
||||||
return !window.storage.get('read-receipt-setting');
|
return !window.storage.get('read-receipt-setting');
|
||||||
case MessageReceiptType.View:
|
case messageReceiptTypeSchema.Enum.View:
|
||||||
if (isStory(message)) {
|
if (isStory(message)) {
|
||||||
return !window.Events.getStoryViewReceiptsEnabled();
|
return !window.Events.getStoryViewReceiptsEnabled();
|
||||||
}
|
}
|
||||||
|
@ -388,9 +395,9 @@ const shouldDropReceipt = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function forMessage(
|
export async function forMessage(
|
||||||
message: MessageModel
|
message: MessageModel
|
||||||
): Array<MessageReceiptAttributesType> {
|
): Promise<Array<MessageReceiptAttributesType>> {
|
||||||
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -408,20 +415,23 @@ export function forMessage(
|
||||||
const receiptValues = Array.from(cachedReceipts.values());
|
const receiptValues = Array.from(cachedReceipts.values());
|
||||||
|
|
||||||
const sentAt = getMessageSentTimestamp(message.attributes, { log });
|
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) {
|
if (result.length > 0) {
|
||||||
log.info(`${logId}: found early receipts for message ${sentAt}`);
|
log.info(`${logId}: found early receipts for message ${sentAt}`);
|
||||||
result.forEach(receipt => {
|
await Promise.all(
|
||||||
remove(receipt);
|
result.map(async receipt => {
|
||||||
});
|
await remove(receipt);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.filter(receipt => {
|
return result.filter(receipt => {
|
||||||
if (shouldDropReceipt(receipt, message.attributes)) {
|
if (shouldDropReceipt(receipt, message.attributes)) {
|
||||||
log.info(
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,7 +443,7 @@ function getNewSendStateByConversationId(
|
||||||
oldSendStateByConversationId: SendStateByConversationId,
|
oldSendStateByConversationId: SendStateByConversationId,
|
||||||
receipt: MessageReceiptAttributesType
|
receipt: MessageReceiptAttributesType
|
||||||
): SendStateByConversationId {
|
): SendStateByConversationId {
|
||||||
const { receiptTimestamp, sourceConversationId, type } = receipt;
|
const { receiptTimestamp, sourceConversationId, type } = receipt.receiptSync;
|
||||||
const oldSendState = getOwn(
|
const oldSendState = getOwn(
|
||||||
oldSendStateByConversationId,
|
oldSendStateByConversationId,
|
||||||
sourceConversationId
|
sourceConversationId
|
||||||
|
@ -441,13 +451,13 @@ function getNewSendStateByConversationId(
|
||||||
|
|
||||||
let sendActionType: SendActionType;
|
let sendActionType: SendActionType;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageReceiptType.Delivery:
|
case messageReceiptTypeSchema.enum.Delivery:
|
||||||
sendActionType = SendActionType.GotDeliveryReceipt;
|
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||||
break;
|
break;
|
||||||
case MessageReceiptType.Read:
|
case messageReceiptTypeSchema.enum.Read:
|
||||||
sendActionType = SendActionType.GotReadReceipt;
|
sendActionType = SendActionType.GotReadReceipt;
|
||||||
break;
|
break;
|
||||||
case MessageReceiptType.View:
|
case messageReceiptTypeSchema.enum.View:
|
||||||
sendActionType = SendActionType.GotViewedReceipt;
|
sendActionType = SendActionType.GotViewedReceipt;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -467,7 +477,7 @@ function updateMessageSendStateWithReceipt(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
receipt: MessageReceiptAttributesType
|
receipt: MessageReceiptAttributesType
|
||||||
): Partial<MessageAttributesType> {
|
): Partial<MessageAttributesType> {
|
||||||
const { messageSentAt } = receipt;
|
const { messageSentAt } = receipt.receiptSync;
|
||||||
|
|
||||||
const newAttributes: Partial<MessageAttributesType> = {};
|
const newAttributes: Partial<MessageAttributesType> = {};
|
||||||
|
|
||||||
|
@ -510,27 +520,34 @@ async function addToDeleteSentProtoBatcher(
|
||||||
receipt: MessageReceiptAttributesType,
|
receipt: MessageReceiptAttributesType,
|
||||||
message: MessageAttributesType
|
message: MessageAttributesType
|
||||||
) {
|
) {
|
||||||
const { sourceConversationId, type } = receipt;
|
const { receiptSync } = receipt;
|
||||||
|
const {
|
||||||
|
sourceConversationId,
|
||||||
|
type,
|
||||||
|
wasSentEncrypted,
|
||||||
|
messageSentAt,
|
||||||
|
sourceDevice,
|
||||||
|
} = receiptSync;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(type === MessageReceiptType.Delivery &&
|
(type === messageReceiptTypeSchema.enum.Delivery &&
|
||||||
wasDeliveredWithSealedSender(sourceConversationId, message) &&
|
wasDeliveredWithSealedSender(sourceConversationId, message) &&
|
||||||
receipt.wasSentEncrypted) ||
|
wasSentEncrypted) ||
|
||||||
type === MessageReceiptType.Read
|
type === messageReceiptTypeSchema.enum.Read
|
||||||
) {
|
) {
|
||||||
const recipient = window.ConversationController.get(sourceConversationId);
|
const recipient = window.ConversationController.get(sourceConversationId);
|
||||||
const recipientServiceId = recipient?.getServiceId();
|
const recipientServiceId = recipient?.getServiceId();
|
||||||
const deviceId = receipt.sourceDevice;
|
const deviceId = sourceDevice;
|
||||||
|
|
||||||
if (recipientServiceId && deviceId) {
|
if (recipientServiceId && deviceId) {
|
||||||
await deleteSentProtoBatcher.add({
|
await deleteSentProtoBatcher.add({
|
||||||
timestamp: receipt.messageSentAt,
|
timestamp: messageSentAt,
|
||||||
recipientServiceId,
|
recipientServiceId,
|
||||||
deviceId,
|
deviceId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
`MessageReceipts.deleteSentProto(sentAt=${receipt.messageSentAt}): ` +
|
`MessageReceipts.deleteSentProto(sentAt=${messageSentAt}): ` +
|
||||||
`Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
|
`Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -540,6 +557,6 @@ async function addToDeleteSentProtoBatcher(
|
||||||
export async function onReceipt(
|
export async function onReceipt(
|
||||||
receipt: MessageReceiptAttributesType
|
receipt: MessageReceiptAttributesType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
cachedReceipts.set(getReceiptCacheKey(receipt), receipt);
|
cachedReceipts.set(receipt.syncTaskId, receipt);
|
||||||
await processReceiptBatcher.add(receipt);
|
await processReceiptBatcher.add(receipt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
// Copyright 2017 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { AciString } from '../types/ServiceId';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
@ -14,58 +15,69 @@ import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||||
import { strictAssert } from '../util/assert';
|
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 = {
|
export type ReadSyncAttributesType = {
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
readAt: number;
|
syncTaskId: string;
|
||||||
removeFromMessageReceiverCache: () => unknown;
|
readSync: ReadSyncTaskType;
|
||||||
sender?: string;
|
|
||||||
senderAci: AciString;
|
|
||||||
senderId: string;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const readSyncs = new Map<string, ReadSyncAttributesType>();
|
const readSyncs = new Map<string, ReadSyncAttributesType>();
|
||||||
|
|
||||||
function remove(sync: ReadSyncAttributesType): void {
|
async function remove(sync: ReadSyncAttributesType): Promise<void> {
|
||||||
readSyncs.delete(
|
const { syncTaskId } = sync;
|
||||||
generateCacheKey({
|
readSyncs.delete(syncTaskId);
|
||||||
sender: sync.senderId,
|
await removeSyncTaskById(syncTaskId);
|
||||||
timestamp: sync.timestamp,
|
|
||||||
type: 'readsync',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
sync.removeFromMessageReceiverCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function maybeItIsAReactionReadSync(
|
async function maybeItIsAReactionReadSync(
|
||||||
sync: ReadSyncAttributesType
|
sync: ReadSyncAttributesType
|
||||||
): Promise<void> {
|
): 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(
|
const readReaction = await window.Signal.Data.markReactionAsRead(
|
||||||
sync.senderAci,
|
readSync.senderAci,
|
||||||
Number(sync.timestamp)
|
Number(readSync.timestamp)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!readReaction ||
|
!readReaction ||
|
||||||
readReaction?.targetAuthorAci !== window.storage.user.getCheckedAci()
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`${logId} read reaction sync found:`,
|
`${logId} read reaction sync found:`,
|
||||||
readReaction.conversationId,
|
readReaction.conversationId,
|
||||||
sync.senderId,
|
readSync.senderId,
|
||||||
sync.sender,
|
readSync.sender,
|
||||||
sync.senderAci
|
readSync.senderAci
|
||||||
);
|
);
|
||||||
|
|
||||||
remove(sync);
|
await remove(sync);
|
||||||
|
|
||||||
notificationService.removeBy({
|
notificationService.removeBy({
|
||||||
conversationId: readReaction.conversationId,
|
conversationId: readReaction.conversationId,
|
||||||
|
@ -75,9 +87,9 @@ async function maybeItIsAReactionReadSync(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forMessage(
|
export async function forMessage(
|
||||||
message: MessageModel
|
message: MessageModel
|
||||||
): ReadSyncAttributesType | null {
|
): Promise<ReadSyncAttributesType | null> {
|
||||||
const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
|
const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
|
||||||
message.attributes
|
message.attributes
|
||||||
)})`;
|
)})`;
|
||||||
|
@ -92,13 +104,17 @@ export function forMessage(
|
||||||
});
|
});
|
||||||
const readSyncValues = Array.from(readSyncs.values());
|
const readSyncValues = Array.from(readSyncs.values());
|
||||||
const foundSync = readSyncValues.find(item => {
|
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) {
|
if (foundSync) {
|
||||||
log.info(
|
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;
|
return foundSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,20 +122,15 @@ export function forMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||||
readSyncs.set(
|
const { readSync, syncTaskId } = sync;
|
||||||
generateCacheKey({
|
|
||||||
sender: sync.senderId,
|
|
||||||
timestamp: sync.timestamp,
|
|
||||||
type: 'readsync',
|
|
||||||
}),
|
|
||||||
sync
|
|
||||||
);
|
|
||||||
|
|
||||||
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
|
readSyncs.set(syncTaskId, sync);
|
||||||
|
|
||||||
|
const logId = `ReadSyncs.onSync(timestamp=${readSync.timestamp})`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
sync.timestamp
|
readSync.timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
const found = messages.find(item => {
|
const found = messages.find(item => {
|
||||||
|
@ -129,7 +140,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||||
reason: logId,
|
reason: logId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return isIncoming(item) && sender?.id === sync.senderId;
|
return isIncoming(item) && sender?.id === readSync.senderId;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
@ -144,8 +155,8 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||||
found,
|
found,
|
||||||
'ReadSyncs.onSync'
|
'ReadSyncs.onSync'
|
||||||
);
|
);
|
||||||
const readAt = Math.min(sync.readAt, Date.now());
|
const readAt = Math.min(readSync.readAt, Date.now());
|
||||||
const newestSentAt = sync.timestamp;
|
const newestSentAt = readSync.timestamp;
|
||||||
|
|
||||||
// If message is unread, we mark it read. Otherwise, we update the expiration
|
// 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
|
// 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);
|
queueUpdateMessage(message.attributes);
|
||||||
|
|
||||||
remove(sync);
|
await remove(sync);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
remove(sync);
|
|
||||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||||
|
await remove(sync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { AciString } from '../types/ServiceId';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
@ -15,35 +16,38 @@ import { markViewed } from '../services/MessageUpdater';
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||||
import { generateCacheKey } from './generateCacheKey';
|
|
||||||
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
|
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 = {
|
export type ViewSyncAttributesType = {
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
removeFromMessageReceiverCache: () => unknown;
|
syncTaskId: string;
|
||||||
senderAci: AciString;
|
viewSync: ViewSyncTaskType;
|
||||||
senderE164?: string;
|
|
||||||
senderId: string;
|
|
||||||
timestamp: number;
|
|
||||||
viewedAt: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewSyncs = new Map<string, ViewSyncAttributesType>();
|
const viewSyncs = new Map<string, ViewSyncAttributesType>();
|
||||||
|
|
||||||
function remove(sync: ViewSyncAttributesType): void {
|
async function remove(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
viewSyncs.delete(
|
await removeSyncTaskById(sync.syncTaskId);
|
||||||
generateCacheKey({
|
|
||||||
sender: sync.senderId,
|
|
||||||
timestamp: sync.timestamp,
|
|
||||||
type: 'viewsync',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
sync.removeFromMessageReceiverCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forMessage(
|
export async function forMessage(
|
||||||
message: MessageModel
|
message: MessageModel
|
||||||
): Array<ViewSyncAttributesType> {
|
): Promise<Array<ViewSyncAttributesType>> {
|
||||||
const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
|
const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
|
||||||
message.attributes
|
message.attributes
|
||||||
)})`;
|
)})`;
|
||||||
|
@ -60,7 +64,11 @@ export function forMessage(
|
||||||
const viewSyncValues = Array.from(viewSyncs.values());
|
const viewSyncValues = Array.from(viewSyncs.values());
|
||||||
|
|
||||||
const matchingSyncs = viewSyncValues.filter(item => {
|
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) {
|
if (matchingSyncs.length > 0) {
|
||||||
|
@ -68,28 +76,24 @@ export function forMessage(
|
||||||
`${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
|
`${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
matchingSyncs.forEach(sync => {
|
await Promise.all(
|
||||||
remove(sync);
|
matchingSyncs.map(async sync => {
|
||||||
});
|
await remove(sync);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return matchingSyncs;
|
return matchingSyncs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
viewSyncs.set(
|
viewSyncs.set(sync.syncTaskId, sync);
|
||||||
generateCacheKey({
|
const { viewSync } = sync;
|
||||||
sender: sync.senderId,
|
|
||||||
timestamp: sync.timestamp,
|
|
||||||
type: 'viewsync',
|
|
||||||
}),
|
|
||||||
sync
|
|
||||||
);
|
|
||||||
|
|
||||||
const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
|
const logId = `ViewSyncs.onSync(timestamp=${viewSync.timestamp})`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
sync.timestamp
|
viewSync.timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
const found = messages.find(item => {
|
const found = messages.find(item => {
|
||||||
|
@ -99,15 +103,15 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
reason: logId,
|
reason: logId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return sender?.id === sync.senderId;
|
return sender?.id === viewSync.senderId;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: nothing found`,
|
`${logId}: nothing found`,
|
||||||
sync.senderId,
|
viewSync.senderId,
|
||||||
sync.senderE164,
|
viewSync.senderE164,
|
||||||
sync.senderAci
|
viewSync.senderAci
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +127,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
|
|
||||||
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
||||||
didChangeMessage = true;
|
didChangeMessage = true;
|
||||||
message.set(markViewed(message.attributes, sync.viewedAt));
|
message.set(markViewed(message.attributes, viewSync.viewedAt));
|
||||||
|
|
||||||
const attachments = message.get('attachments');
|
const attachments = message.get('attachments');
|
||||||
if (!attachments?.every(isDownloaded)) {
|
if (!attachments?.every(isDownloaded)) {
|
||||||
|
@ -154,9 +158,9 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
queueUpdateMessage(message.attributes);
|
queueUpdateMessage(message.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(sync);
|
await remove(sync);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
remove(sync);
|
|
||||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||||
|
await remove(sync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2945,6 +2945,13 @@ export class ConversationModel extends window.Backbone
|
||||||
senderAci,
|
senderAci,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.get('active_at')) {
|
||||||
|
log.warn(
|
||||||
|
`addDeliveryIssue: ${this.idForLogging()} has no active_at, dropping delivery issue instead of adding`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'delivery-issue',
|
type: 'delivery-issue',
|
||||||
|
@ -3363,7 +3370,9 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
||||||
if (message) {
|
if (message) {
|
||||||
await window.Signal.Data.removeMessage(message.id);
|
await window.Signal.Data.removeMessage(message.id, {
|
||||||
|
singleProtoJobQueue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -3404,7 +3413,9 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
||||||
if (message) {
|
if (message) {
|
||||||
await window.Signal.Data.removeMessage(message.id);
|
await window.Signal.Data.removeMessage(message.id, {
|
||||||
|
singleProtoJobQueue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -5003,7 +5014,14 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
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`);
|
log.info(`${logId}: Preparing sync message`);
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
@ -5044,7 +5062,9 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
log.info(`${logId}: Starting delete`);
|
log.info(`${logId}: Starting delete`);
|
||||||
await window.Signal.Data.removeMessagesInConversation(this.id, {
|
await window.Signal.Data.removeMessagesInConversation(this.id, {
|
||||||
|
fromSync: source !== 'local-delete-sync',
|
||||||
logId: this.idForLogging(),
|
logId: this.idForLogging(),
|
||||||
|
singleProtoJobQueue,
|
||||||
});
|
});
|
||||||
log.info(`${logId}: Delete complete`);
|
log.info(`${logId}: Delete complete`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ import {
|
||||||
} from '../services/notifications';
|
} from '../services/notifications';
|
||||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { cleanupMessage, deleteMessageData } from '../util/cleanup';
|
import { deleteMessageData } from '../util/cleanup';
|
||||||
import {
|
import {
|
||||||
getSource,
|
getSource,
|
||||||
getSourceServiceId,
|
getSourceServiceId,
|
||||||
|
@ -315,10 +315,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
this.set(attributes);
|
this.set(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
await cleanupMessage(this.attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteData(): Promise<void> {
|
async deleteData(): Promise<void> {
|
||||||
await deleteMessageData(this.attributes);
|
await deleteMessageData(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,21 @@
|
||||||
import { batch } from 'react-redux';
|
import { batch } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
|
||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import { sleep } from '../util/sleep';
|
import { sleep } from '../util/sleep';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import * as Errors from '../types/errors';
|
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 {
|
class ExpiringMessagesDeletionService {
|
||||||
public update: typeof this.checkExpiringMessages;
|
public update: typeof this.checkExpiringMessages;
|
||||||
|
|
||||||
private timeout?: ReturnType<typeof setTimeout>;
|
private timeout?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly singleProtoJobQueue: SingleProtoJobQueue) {
|
||||||
this.update = debounce(this.checkExpiringMessages, 1000);
|
this.update = debounce(this.checkExpiringMessages, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +45,9 @@ class ExpiringMessagesDeletionService {
|
||||||
inMemoryMessages.push(message);
|
inMemoryMessages.push(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
await window.Signal.Data.removeMessages(messageIds);
|
await window.Signal.Data.removeMessages(messageIds, {
|
||||||
|
singleProtoJobQueue: this.singleProtoJobQueue,
|
||||||
|
});
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
inMemoryMessages.forEach(message => {
|
inMemoryMessages.forEach(message => {
|
||||||
|
@ -108,5 +113,21 @@ class ExpiringMessagesDeletionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expiringMessagesDeletionService =
|
// Because this service is used inside of Client.ts, it can't directly reference
|
||||||
new ExpiringMessagesDeletionService();
|
// 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;
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { ipcRenderer as ipc } from 'electron';
|
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 { has, get, groupBy, isTypedArray, last, map, omit } from 'lodash';
|
||||||
|
|
||||||
import { deleteExternalFiles } from '../types/Conversation';
|
import { deleteExternalFiles } from '../types/Conversation';
|
||||||
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
|
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
|
||||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import { createBatcher } from '../util/batcher';
|
import { createBatcher } from '../util/batcher';
|
||||||
|
@ -24,12 +22,7 @@ import * as Errors from '../types/errors';
|
||||||
|
|
||||||
import type { StoredJob } from '../jobs/types';
|
import type { StoredJob } from '../jobs/types';
|
||||||
import { formatJobForInsert } from '../jobs/formatJobForInsert';
|
import { formatJobForInsert } from '../jobs/formatJobForInsert';
|
||||||
import {
|
import { cleanupMessages } from '../util/cleanup';
|
||||||
cleanupMessage,
|
|
||||||
cleanupMessageFromMemory,
|
|
||||||
deleteMessageData,
|
|
||||||
} from '../util/cleanup';
|
|
||||||
import { drop } from '../util/drop';
|
|
||||||
import { ipcInvoke, doShutdown } from './channels';
|
import { ipcInvoke, doShutdown } from './channels';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -60,12 +53,12 @@ import type {
|
||||||
KyberPreKeyType,
|
KyberPreKeyType,
|
||||||
StoredKyberPreKeyType,
|
StoredKyberPreKeyType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import { MINUTE } from '../util/durations';
|
|
||||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
import type { MessageAttributesType } from '../model-types';
|
import type { MessageAttributesType } from '../model-types';
|
||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
import { generateSnippetAroundMention } from '../util/search';
|
import { generateSnippetAroundMention } from '../util/search';
|
||||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||||
|
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
|
@ -104,6 +97,8 @@ const exclusiveInterface: ClientExclusiveInterface = {
|
||||||
removeConversation,
|
removeConversation,
|
||||||
|
|
||||||
searchMessages,
|
searchMessages,
|
||||||
|
removeMessage,
|
||||||
|
removeMessages,
|
||||||
|
|
||||||
getRecentStoryReplies,
|
getRecentStoryReplies,
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
|
@ -125,8 +120,6 @@ const exclusiveInterface: ClientExclusiveInterface = {
|
||||||
type ClientOverridesType = ClientExclusiveInterface &
|
type ClientOverridesType = ClientExclusiveInterface &
|
||||||
Pick<
|
Pick<
|
||||||
ServerInterface,
|
ServerInterface,
|
||||||
| 'removeMessage'
|
|
||||||
| 'removeMessages'
|
|
||||||
| 'saveAttachmentDownloadJob'
|
| 'saveAttachmentDownloadJob'
|
||||||
| 'saveMessage'
|
| 'saveMessage'
|
||||||
| 'saveMessages'
|
| 'saveMessages'
|
||||||
|
@ -142,8 +135,6 @@ const channels: ServerInterface = new Proxy({} as ServerInterface, {
|
||||||
|
|
||||||
const clientExclusiveOverrides: ClientOverridesType = {
|
const clientExclusiveOverrides: ClientOverridesType = {
|
||||||
...exclusiveInterface,
|
...exclusiveInterface,
|
||||||
removeMessage,
|
|
||||||
removeMessages,
|
|
||||||
saveAttachmentDownloadJob,
|
saveAttachmentDownloadJob,
|
||||||
saveMessage,
|
saveMessage,
|
||||||
saveMessages,
|
saveMessages,
|
||||||
|
@ -562,7 +553,7 @@ async function saveMessage(
|
||||||
|
|
||||||
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
|
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
|
||||||
|
|
||||||
void expiringMessagesDeletionService.update();
|
void updateExpiringMessagesService();
|
||||||
void tapToViewMessagesDeletionService.update();
|
void tapToViewMessagesDeletionService.update();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
@ -577,26 +568,39 @@ async function saveMessages(
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
void expiringMessagesDeletionService.update();
|
void updateExpiringMessagesService();
|
||||||
void tapToViewMessagesDeletionService.update();
|
void tapToViewMessagesDeletionService.update();
|
||||||
|
|
||||||
return result;
|
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);
|
const message = await channels.getMessageById(id);
|
||||||
|
|
||||||
// Note: It's important to have a fully database-hydrated model to delete here because
|
// 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.
|
// it needs to delete all associated on-disk files along with the database delete.
|
||||||
if (message) {
|
if (message) {
|
||||||
await channels.removeMessage(id);
|
await channels.removeMessage(id);
|
||||||
await cleanupMessage(message);
|
await cleanupMessages([message], {
|
||||||
|
...options,
|
||||||
|
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAndCleanup(
|
export async function deleteAndCleanup(
|
||||||
messages: Array<MessageAttributesType>,
|
messages: Array<MessageAttributesType>,
|
||||||
logId: string
|
logId: string,
|
||||||
|
options: {
|
||||||
|
fromSync?: boolean;
|
||||||
|
singleProtoJobQueue: SingleProtoJobQueue;
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const ids = messages.map(message => message.id);
|
const ids = messages.map(message => message.id);
|
||||||
|
|
||||||
|
@ -604,37 +608,26 @@ export async function deleteAndCleanup(
|
||||||
await channels.removeMessages(ids);
|
await channels.removeMessages(ids);
|
||||||
|
|
||||||
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
|
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
|
||||||
await _cleanupMessages(messages);
|
await cleanupMessages(messages, {
|
||||||
|
...options,
|
||||||
|
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
log.info(`deleteAndCleanup/${logId}: Complete`);
|
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(
|
async function removeMessages(
|
||||||
messageIds: ReadonlyArray<string>
|
messageIds: ReadonlyArray<string>,
|
||||||
|
options: {
|
||||||
|
fromSync?: boolean;
|
||||||
|
singleProtoJobQueue: SingleProtoJobQueue;
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const messages = await channels.getMessagesById(messageIds);
|
const messages = await channels.getMessagesById(messageIds);
|
||||||
await _cleanupMessages(messages);
|
await cleanupMessages(messages, {
|
||||||
|
...options,
|
||||||
|
markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
|
||||||
|
});
|
||||||
await channels.removeMessages(messageIds);
|
await channels.removeMessages(messageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -686,9 +679,13 @@ async function removeMessagesInConversation(
|
||||||
{
|
{
|
||||||
logId,
|
logId,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
|
singleProtoJobQueue,
|
||||||
|
fromSync,
|
||||||
}: {
|
}: {
|
||||||
|
fromSync?: boolean;
|
||||||
logId: string;
|
logId: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
|
singleProtoJobQueue: SingleProtoJobQueue;
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let messages;
|
let messages;
|
||||||
|
@ -713,7 +710,7 @@ async function removeMessagesInConversation(
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await deleteAndCleanup(messages, logId);
|
await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue });
|
||||||
} while (messages.length > 0);
|
} while (messages.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||||
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
|
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
|
||||||
import type { SyncTaskType } from '../util/syncTasks';
|
import type { SyncTaskType } from '../util/syncTasks';
|
||||||
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
||||||
|
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
export type AdjacentMessagesByConversationOptionsType = Readonly<{
|
export type AdjacentMessagesByConversationOptionsType = Readonly<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -557,8 +558,6 @@ export type DataInterface = {
|
||||||
arrayOfMessages: ReadonlyArray<MessageType>,
|
arrayOfMessages: ReadonlyArray<MessageType>,
|
||||||
options: { forceSave?: boolean; ourAci: AciString }
|
options: { forceSave?: boolean; ourAci: AciString }
|
||||||
) => Promise<Array<string>>;
|
) => Promise<Array<string>>;
|
||||||
removeMessage: (id: string) => Promise<void>;
|
|
||||||
removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
|
|
||||||
pageMessages: (
|
pageMessages: (
|
||||||
cursor?: PageMessagesCursorType
|
cursor?: PageMessagesCursorType
|
||||||
) => Promise<PageMessagesResultType>;
|
) => Promise<PageMessagesResultType>;
|
||||||
|
@ -667,6 +666,7 @@ export type DataInterface = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
}): Promise<MessageType | undefined>;
|
}): Promise<MessageType | undefined>;
|
||||||
getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
|
getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
|
||||||
|
markCallHistoryDeleted: (callId: string) => Promise<void>;
|
||||||
clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
|
clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
|
||||||
cleanupCallHistoryMessages: () => Promise<void>;
|
cleanupCallHistoryMessages: () => Promise<void>;
|
||||||
getCallHistoryUnreadCount(): Promise<number>;
|
getCallHistoryUnreadCount(): Promise<number>;
|
||||||
|
@ -929,6 +929,8 @@ export type ServerInterface = DataInterface & {
|
||||||
options?: { limit?: number };
|
options?: { limit?: number };
|
||||||
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
|
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
|
||||||
}) => Promise<Array<ServerSearchResultMessageType>>;
|
}) => Promise<Array<ServerSearchResultMessageType>>;
|
||||||
|
removeMessage: (id: string) => Promise<void>;
|
||||||
|
removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
|
||||||
|
|
||||||
getRecentStoryReplies(
|
getRecentStoryReplies(
|
||||||
storyId: string,
|
storyId: string,
|
||||||
|
@ -1022,6 +1024,20 @@ export type ClientExclusiveInterface = {
|
||||||
removeConversation: (id: string) => Promise<void>;
|
removeConversation: (id: string) => Promise<void>;
|
||||||
flushUpdateConversationBatcher: () => 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: ({
|
searchMessages: ({
|
||||||
query,
|
query,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -1084,8 +1100,10 @@ export type ClientExclusiveInterface = {
|
||||||
removeMessagesInConversation: (
|
removeMessagesInConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: {
|
options: {
|
||||||
|
fromSync?: boolean;
|
||||||
logId: string;
|
logId: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
|
singleProtoJobQueue: SingleProtoJobQueue;
|
||||||
}
|
}
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
removeOtherData: () => Promise<void>;
|
removeOtherData: () => Promise<void>;
|
||||||
|
|
|
@ -347,6 +347,7 @@ const dataInterface: ServerInterface = {
|
||||||
getLastConversationMessage,
|
getLastConversationMessage,
|
||||||
getAllCallHistory,
|
getAllCallHistory,
|
||||||
clearCallHistory,
|
clearCallHistory,
|
||||||
|
markCallHistoryDeleted,
|
||||||
cleanupCallHistoryMessages,
|
cleanupCallHistoryMessages,
|
||||||
getCallHistoryUnreadCount,
|
getCallHistoryUnreadCount,
|
||||||
markCallHistoryRead,
|
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> {
|
async function cleanupCallHistoryMessages(): Promise<void> {
|
||||||
const db = await getWritableInstance();
|
const db = await getWritableInstance();
|
||||||
return db
|
return db
|
||||||
|
|
|
@ -195,6 +195,7 @@ import {
|
||||||
} from '../../util/deleteForMe';
|
} from '../../util/deleteForMe';
|
||||||
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
|
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
|
||||||
import { isEnabled } from '../../RemoteConfig';
|
import { isEnabled } from '../../RemoteConfig';
|
||||||
|
import type { CapabilitiesType } from '../../textsecure/WebAPI';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -265,6 +266,7 @@ export type ConversationType = ReadonlyDeep<
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
profileLastUpdatedAt?: number;
|
profileLastUpdatedAt?: number;
|
||||||
|
capabilities?: CapabilitiesType;
|
||||||
username?: string;
|
username?: string;
|
||||||
about?: string;
|
about?: string;
|
||||||
aboutText?: 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);
|
popPanelForConversation()(dispatch, getState, undefined);
|
||||||
|
|
||||||
|
@ -1761,7 +1765,11 @@ function deleteMessages({
|
||||||
dispatch(scrollToMessage(conversationId, nearbyMessageId));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
|
|
|
@ -69,6 +69,7 @@ import {
|
||||||
conversationQueueJobEnum,
|
conversationQueueJobEnum,
|
||||||
} from '../../jobs/conversationJobQueue';
|
} from '../../jobs/conversationJobQueue';
|
||||||
import { ReceiptType } from '../../types/Receipt';
|
import { ReceiptType } from '../../types/Receipt';
|
||||||
|
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
export type StoryDataType = ReadonlyDeep<
|
export type StoryDataType = ReadonlyDeep<
|
||||||
{
|
{
|
||||||
|
@ -284,7 +285,7 @@ function deleteGroupStoryReply(
|
||||||
messageId: string
|
messageId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
|
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
await window.Signal.Data.removeMessage(messageId);
|
await window.Signal.Data.removeMessage(messageId, { singleProtoJobQueue });
|
||||||
dispatch({
|
dispatch({
|
||||||
type: STORY_REPLY_DELETED,
|
type: STORY_REPLY_DELETED,
|
||||||
payload: messageId,
|
payload: messageId,
|
||||||
|
@ -1408,10 +1409,7 @@ function removeAllContactStories(
|
||||||
|
|
||||||
log.info(`${logId}: removing ${messages.length} stories`);
|
log.info(`${logId}: removing ${messages.length} stories`);
|
||||||
|
|
||||||
await Promise.all([
|
await dataInterface.removeMessages(messageIds, { singleProtoJobQueue });
|
||||||
messages.map(m => m.cleanup()),
|
|
||||||
await dataInterface.removeMessages(messageIds),
|
|
||||||
]);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'NOOP',
|
type: 'NOOP',
|
||||||
|
|
38
ts/state/selectors/items-extra.ts
Normal file
38
ts/state/selectors/items-extra.ts
Normal 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');
|
||||||
|
}
|
||||||
|
);
|
|
@ -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
|
// Note: ts/util/stories is the other place this check is done
|
||||||
export const getStoriesEnabled = createSelector(
|
export const getStoriesEnabled = createSelector(
|
||||||
getItems,
|
getItems,
|
||||||
|
|
|
@ -41,10 +41,8 @@ import {
|
||||||
import { getHasStoriesSelector } from '../selectors/stories2';
|
import { getHasStoriesSelector } from '../selectors/stories2';
|
||||||
import { getIntl, getTheme, getUserACI } from '../selectors/user';
|
import { getIntl, getTheme, getUserACI } from '../selectors/user';
|
||||||
import { useItemsActions } from '../ducks/items';
|
import { useItemsActions } from '../ducks/items';
|
||||||
import {
|
import { getLocalDeleteWarningShown } from '../selectors/items';
|
||||||
getDeleteSyncSendEnabled,
|
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
|
||||||
getLocalDeleteWarningShown,
|
|
||||||
} from '../selectors/items';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -17,10 +17,8 @@ import {
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { getDeleteMessagesProps } from '../selectors/globalModals';
|
import { getDeleteMessagesProps } from '../selectors/globalModals';
|
||||||
import { useItemsActions } from '../ducks/items';
|
import { useItemsActions } from '../ducks/items';
|
||||||
import {
|
import { getLocalDeleteWarningShown } from '../selectors/items';
|
||||||
getLocalDeleteWarningShown,
|
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
|
||||||
getDeleteSyncSendEnabled,
|
|
||||||
} from '../selectors/items';
|
|
||||||
import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
|
import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
|
||||||
|
|
||||||
export const SmartDeleteMessagesModal = memo(
|
export const SmartDeleteMessagesModal = memo(
|
||||||
|
|
|
@ -59,9 +59,9 @@ describe('filterAndSortConversations', () => {
|
||||||
check({
|
check({
|
||||||
searchTerm: '9876',
|
searchTerm: '9876',
|
||||||
input: [
|
input: [
|
||||||
{ title: 'no' },
|
{ title: 'no', e164: undefined },
|
||||||
{ title: 'yes', e164: '+16505559876' },
|
{ title: 'yes', e164: '+16505559876' },
|
||||||
{ title: 'no' },
|
{ title: 'no', e164: undefined },
|
||||||
],
|
],
|
||||||
expected: [{ title: 'yes' }],
|
expected: [{ title: 'yes' }],
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,10 +7,13 @@ import { assert } from 'chai';
|
||||||
import { type AciString, generateAci } from '../types/ServiceId';
|
import { type AciString, generateAci } from '../types/ServiceId';
|
||||||
import type { MessageAttributesType } from '../model-types';
|
import type { MessageAttributesType } from '../model-types';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
import {
|
import type {
|
||||||
type MessageReceiptAttributesType,
|
MessageReceiptAttributesType,
|
||||||
MessageReceiptType,
|
MessageReceiptType,
|
||||||
|
} from '../messageModifiers/MessageReceipts';
|
||||||
|
import {
|
||||||
onReceipt,
|
onReceipt,
|
||||||
|
messageReceiptTypeSchema,
|
||||||
} from '../messageModifiers/MessageReceipts';
|
} from '../messageModifiers/MessageReceipts';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
|
||||||
|
@ -31,14 +34,16 @@ describe('MessageReceipts', () => {
|
||||||
): MessageReceiptAttributesType {
|
): MessageReceiptAttributesType {
|
||||||
return {
|
return {
|
||||||
envelopeId: uuid(),
|
envelopeId: uuid(),
|
||||||
|
syncTaskId: uuid(),
|
||||||
|
receiptSync: {
|
||||||
messageSentAt,
|
messageSentAt,
|
||||||
receiptTimestamp: 1,
|
receiptTimestamp: 1,
|
||||||
removeFromMessageReceiverCache: () => null,
|
|
||||||
sourceConversationId,
|
sourceConversationId,
|
||||||
sourceDevice: 1,
|
sourceDevice: 1,
|
||||||
sourceServiceId: generateAci(),
|
sourceServiceId: generateAci(),
|
||||||
type,
|
type,
|
||||||
wasSentEncrypted: true,
|
wasSentEncrypted: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
it('processes all receipts in a batch', async () => {
|
it('processes all receipts in a batch', async () => {
|
||||||
|
@ -78,10 +83,18 @@ describe('MessageReceipts', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
|
onReceipt(
|
||||||
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
|
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
|
||||||
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
|
),
|
||||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
|
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);
|
const messageFromDatabase = await window.Signal.Data.getMessageById(id);
|
||||||
|
@ -154,20 +167,48 @@ describe('MessageReceipts', () => {
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// send receipts for original message
|
// send receipts for original message
|
||||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
|
onReceipt(
|
||||||
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
|
generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
|
||||||
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
|
),
|
||||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
|
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
|
// and send receipts for edited message
|
||||||
onReceipt(
|
onReceipt(
|
||||||
generateReceipt('aaaa', editedSentAt, MessageReceiptType.Delivery)
|
generateReceipt(
|
||||||
|
'aaaa',
|
||||||
|
editedSentAt,
|
||||||
|
messageReceiptTypeSchema.enum.Delivery
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onReceipt(
|
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);
|
const messageFromDatabase = await window.Signal.Data.getMessageById(id);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { generateAci } from '../../types/ServiceId';
|
import { generateAci } from '../../types/ServiceId';
|
||||||
import { constantTimeEqual, getRandomBytes } from '../../Crypto';
|
import { constantTimeEqual, getRandomBytes } from '../../Crypto';
|
||||||
|
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
_getAllSentProtoMessageIds,
|
_getAllSentProtoMessageIds,
|
||||||
|
@ -148,7 +149,7 @@ describe('sql/sendLog', () => {
|
||||||
|
|
||||||
assert.strictEqual(actual.timestamp, proto.timestamp);
|
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||||
|
|
||||||
await removeMessage(id);
|
await removeMessage(id, { singleProtoJobQueue });
|
||||||
|
|
||||||
assert.lengthOf(await getAllSentProtos(), 0);
|
assert.lengthOf(await getAllSentProtos(), 0);
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
|
||||||
import type { ConversationModel } from '../../models/conversations';
|
import type { ConversationModel } from '../../models/conversations';
|
||||||
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
|
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
|
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
describe('KeyChangeListener', () => {
|
describe('KeyChangeListener', () => {
|
||||||
let oldNumberId: string | undefined;
|
let oldNumberId: string | undefined;
|
||||||
|
@ -69,6 +70,7 @@ describe('KeyChangeListener', () => {
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await window.Signal.Data.removeMessagesInConversation(convo.id, {
|
await window.Signal.Data.removeMessagesInConversation(convo.id, {
|
||||||
logId: ourServiceIdWithKeyChange,
|
logId: ourServiceIdWithKeyChange,
|
||||||
|
singleProtoJobQueue,
|
||||||
});
|
});
|
||||||
await window.Signal.Data.removeConversation(convo.id);
|
await window.Signal.Data.removeConversation(convo.id);
|
||||||
|
|
||||||
|
@ -106,6 +108,7 @@ describe('KeyChangeListener', () => {
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
|
await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
|
||||||
logId: ourServiceIdWithKeyChange,
|
logId: ourServiceIdWithKeyChange,
|
||||||
|
singleProtoJobQueue,
|
||||||
});
|
});
|
||||||
await window.Signal.Data.removeConversation(groupConvo.id);
|
await window.Signal.Data.removeConversation(groupConvo.id);
|
||||||
});
|
});
|
||||||
|
|
|
@ -137,6 +137,8 @@ import type {
|
||||||
DeleteForMeSyncEventData,
|
DeleteForMeSyncEventData,
|
||||||
DeleteForMeSyncTarget,
|
DeleteForMeSyncTarget,
|
||||||
ConversationToDelete,
|
ConversationToDelete,
|
||||||
|
ViewSyncEventData,
|
||||||
|
ReadSyncEventData,
|
||||||
} from './messageReceiverEvents';
|
} from './messageReceiverEvents';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
|
@ -1728,15 +1730,17 @@ export default class MessageReceiver
|
||||||
await this.dispatchAndWait(
|
await this.dispatchAndWait(
|
||||||
getEnvelopeId(envelope),
|
getEnvelopeId(envelope),
|
||||||
new DeliveryEvent(
|
new DeliveryEvent(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
envelopeId: envelope.id,
|
|
||||||
timestamp: envelope.timestamp,
|
timestamp: envelope.timestamp,
|
||||||
envelopeTimestamp: envelope.timestamp,
|
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
sourceServiceId: envelope.sourceServiceId,
|
sourceServiceId: envelope.sourceServiceId,
|
||||||
sourceDevice: envelope.sourceDevice,
|
sourceDevice: envelope.sourceDevice,
|
||||||
wasSentEncrypted: false,
|
wasSentEncrypted: false,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
envelope.id,
|
||||||
|
envelope.timestamp,
|
||||||
this.removeFromCache.bind(this, envelope)
|
this.removeFromCache.bind(this, envelope)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -2907,22 +2911,22 @@ export default class MessageReceiver
|
||||||
|
|
||||||
const logId = getEnvelopeId(envelope);
|
const logId = getEnvelopeId(envelope);
|
||||||
|
|
||||||
await Promise.all(
|
const receipts = receiptMessage.timestamp.map(rawTimestamp => ({
|
||||||
receiptMessage.timestamp.map(async rawTimestamp => {
|
|
||||||
const ev = new EventClass(
|
|
||||||
{
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
timestamp: rawTimestamp?.toNumber(),
|
timestamp: rawTimestamp?.toNumber(),
|
||||||
envelopeTimestamp: envelope.timestamp,
|
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
sourceServiceId: envelope.sourceServiceId,
|
sourceServiceId: envelope.sourceServiceId,
|
||||||
sourceDevice: envelope.sourceDevice,
|
sourceDevice: envelope.sourceDevice,
|
||||||
wasSentEncrypted: true,
|
wasSentEncrypted: true as const,
|
||||||
},
|
}));
|
||||||
|
|
||||||
|
await this.dispatchAndWait(
|
||||||
|
logId,
|
||||||
|
new EventClass(
|
||||||
|
receipts,
|
||||||
|
envelope.id,
|
||||||
|
envelope.timestamp,
|
||||||
this.removeFromCache.bind(this, envelope)
|
this.removeFromCache.bind(this, envelope)
|
||||||
);
|
)
|
||||||
await this.dispatchAndWait(logId, ev);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3469,10 +3473,8 @@ export default class MessageReceiver
|
||||||
|
|
||||||
logUnexpectedUrgentValue(envelope, 'readSync');
|
logUnexpectedUrgentValue(envelope, 'readSync');
|
||||||
|
|
||||||
const results = [];
|
const reads = read.map(
|
||||||
for (const { timestamp, sender, senderAci } of read) {
|
({ timestamp, sender, senderAci }): ReadSyncEventData => ({
|
||||||
const ev = new ReadSyncEvent(
|
|
||||||
{
|
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
timestamp: timestamp?.toNumber(),
|
timestamp: timestamp?.toNumber(),
|
||||||
|
@ -3480,12 +3482,18 @@ export default class MessageReceiver
|
||||||
senderAci: senderAci
|
senderAci: senderAci
|
||||||
? normalizeAci(senderAci, 'handleRead.senderAci')
|
? normalizeAci(senderAci, 'handleRead.senderAci')
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
})
|
||||||
this.removeFromCache.bind(this, envelope)
|
);
|
||||||
|
|
||||||
|
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(
|
private async handleViewed(
|
||||||
|
@ -3497,23 +3505,25 @@ export default class MessageReceiver
|
||||||
|
|
||||||
logUnexpectedUrgentValue(envelope, 'viewSync');
|
logUnexpectedUrgentValue(envelope, 'viewSync');
|
||||||
|
|
||||||
await Promise.all(
|
const views = viewed.map(
|
||||||
viewed.map(async ({ timestamp, senderE164, senderAci }) => {
|
({ timestamp, senderE164, senderAci }): ViewSyncEventData => ({
|
||||||
const ev = new ViewSyncEvent(
|
|
||||||
{
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
envelopeTimestamp: envelope.timestamp,
|
|
||||||
timestamp: timestamp?.toNumber(),
|
timestamp: timestamp?.toNumber(),
|
||||||
senderE164: dropNull(senderE164),
|
senderE164: dropNull(senderE164),
|
||||||
senderAci: senderAci
|
senderAci: senderAci
|
||||||
? normalizeAci(senderAci, 'handleViewed.senderAci')
|
? normalizeAci(senderAci, 'handleViewed.senderAci')
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
|
||||||
this.removeFromCache.bind(this, envelope)
|
|
||||||
);
|
|
||||||
await this.dispatchAndWait(logId, ev);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.dispatchAndWait(
|
||||||
|
logId,
|
||||||
|
new ViewSyncEvent(
|
||||||
|
views,
|
||||||
|
envelope.id,
|
||||||
|
envelope.timestamp,
|
||||||
|
this.removeFromCache.bind(this, envelope)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCallEvent(
|
private async handleCallEvent(
|
||||||
|
@ -3663,7 +3673,19 @@ export default class MessageReceiver
|
||||||
? processConversationToDelete(item.conversation, logId)
|
? processConversationToDelete(item.conversation, logId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (messages?.length && conversation) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// We want each message in its own task
|
// We want each message in its own task
|
||||||
return messages.map(innerItem => {
|
return messages.map(innerItem => {
|
||||||
return {
|
return {
|
||||||
|
@ -3673,9 +3695,6 @@ export default class MessageReceiver
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})
|
})
|
||||||
.filter(isNotNil);
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
@ -3692,7 +3711,19 @@ export default class MessageReceiver
|
||||||
? processConversationToDelete(item.conversation, logId)
|
? processConversationToDelete(item.conversation, logId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (mostRecentMessages?.length && conversation) {
|
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 {
|
return {
|
||||||
type: 'delete-conversation' as const,
|
type: 'delete-conversation' as const,
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -3700,9 +3731,6 @@ export default class MessageReceiver
|
||||||
mostRecentMessages,
|
mostRecentMessages,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})
|
})
|
||||||
.filter(isNotNil);
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
@ -3716,15 +3744,18 @@ export default class MessageReceiver
|
||||||
? processConversationToDelete(item.conversation, logId)
|
? processConversationToDelete(item.conversation, logId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (conversation) {
|
if (!conversation) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}/handleDeleteForMeSync/localOnlyConversationDeletes: No target conversation`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'delete-local-conversation' as const,
|
type: 'delete-local-conversation' as const,
|
||||||
conversation,
|
conversation,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})
|
})
|
||||||
.filter(isNotNil);
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
@ -3969,16 +4000,33 @@ function processMessageToDelete(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.authorAci) {
|
const { authorServiceId } = target;
|
||||||
|
if (authorServiceId) {
|
||||||
|
if (isAciString(authorServiceId)) {
|
||||||
return {
|
return {
|
||||||
type: 'aci' as const,
|
type: 'aci' as const,
|
||||||
authorAci: normalizeAci(
|
authorAci: normalizeAci(
|
||||||
target.authorAci,
|
authorServiceId,
|
||||||
`${logId}/processMessageToDelete`
|
`${logId}/processMessageToDelete/aci`
|
||||||
),
|
),
|
||||||
sentAt,
|
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) {
|
if (target.authorE164) {
|
||||||
return {
|
return {
|
||||||
type: 'e164' as const,
|
type: 'e164' as const,
|
||||||
|
@ -3997,14 +4045,26 @@ function processConversationToDelete(
|
||||||
target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
|
target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
|
||||||
logId: string
|
logId: string
|
||||||
): ConversationToDelete | undefined {
|
): ConversationToDelete | undefined {
|
||||||
const { threadAci, threadGroupId, threadE164 } = target;
|
const { threadServiceId, threadGroupId, threadE164 } = target;
|
||||||
|
|
||||||
if (threadAci) {
|
if (threadServiceId) {
|
||||||
|
if (isAciString(threadServiceId)) {
|
||||||
return {
|
return {
|
||||||
type: 'aci' as const,
|
type: 'aci' as const,
|
||||||
aci: normalizeAci(threadAci, `${logId}/threadAci`),
|
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) {
|
if (threadGroupId) {
|
||||||
return {
|
return {
|
||||||
type: 'group' as const,
|
type: 'group' as const,
|
||||||
|
|
|
@ -89,6 +89,14 @@ import type {
|
||||||
MessageToDelete,
|
MessageToDelete,
|
||||||
} from './messageReceiverEvents';
|
} from './messageReceiverEvents';
|
||||||
import { getConversationFromTarget } from '../util/deleteForMe';
|
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 = {
|
export type SendMetadataType = {
|
||||||
[serviceId: ServiceIdString]: {
|
[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(
|
async syncReadMessages(
|
||||||
reads: ReadonlyArray<{
|
reads: ReadonlyArray<{
|
||||||
senderAci?: AciString;
|
senderAci?: AciString;
|
||||||
|
@ -2353,9 +2426,11 @@ function toAddressableMessage(message: MessageToDelete) {
|
||||||
targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
|
targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
|
||||||
|
|
||||||
if (message.type === 'aci') {
|
if (message.type === 'aci') {
|
||||||
targetMessage.authorAci = message.authorAci;
|
targetMessage.authorServiceId = message.authorAci;
|
||||||
} else if (message.type === 'e164') {
|
} else if (message.type === 'e164') {
|
||||||
targetMessage.authorE164 = message.authorE164;
|
targetMessage.authorE164 = message.authorE164;
|
||||||
|
} else if (message.type === 'pni') {
|
||||||
|
targetMessage.authorServiceId = message.authorPni;
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(message);
|
throw missingCaseError(message);
|
||||||
}
|
}
|
||||||
|
@ -2368,7 +2443,9 @@ function toConversationIdentifier(conversation: ConversationToDelete) {
|
||||||
new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
|
new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
|
||||||
|
|
||||||
if (conversation.type === 'aci') {
|
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') {
|
} else if (conversation.type === 'group') {
|
||||||
targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
|
targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
|
||||||
} else if (conversation.type === 'e164') {
|
} else if (conversation.type === 'e164') {
|
||||||
|
|
|
@ -703,8 +703,12 @@ export type WebAPIConnectType = {
|
||||||
connect: (options: WebAPIConnectOptionsType) => WebAPIType;
|
connect: (options: WebAPIConnectOptionsType) => WebAPIType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CapabilitiesType = Record<string, never>;
|
export type CapabilitiesType = {
|
||||||
export type CapabilitiesUploadType = Record<string, never>;
|
deleteSync: boolean;
|
||||||
|
};
|
||||||
|
export type CapabilitiesUploadType = {
|
||||||
|
deleteSync: true;
|
||||||
|
};
|
||||||
|
|
||||||
type StickerPackManifestType = Uint8Array;
|
type StickerPackManifestType = Uint8Array;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,11 @@ import type { PublicKey } from '@signalapp/libsignal-client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { SignalService as Proto } from '../protobuf';
|
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 { StoryDistributionIdString } from '../types/StoryDistributionId';
|
||||||
import type {
|
import type {
|
||||||
ProcessedEnvelope,
|
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 {
|
export class EnvelopeQueuedEvent extends Event {
|
||||||
constructor(public readonly envelope: ProcessedEnvelope) {
|
constructor(public readonly envelope: ProcessedEnvelope) {
|
||||||
super('envelopeQueued');
|
super('envelopeQueued');
|
||||||
|
@ -113,9 +116,7 @@ export class ConfirmableEvent extends Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeliveryEventData = Readonly<{
|
export type DeliveryEventData = Readonly<{
|
||||||
envelopeId: string;
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
envelopeTimestamp: number;
|
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceServiceId?: ServiceIdString;
|
sourceServiceId?: ServiceIdString;
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
|
@ -124,7 +125,9 @@ export type DeliveryEventData = Readonly<{
|
||||||
|
|
||||||
export class DeliveryEvent extends ConfirmableEvent {
|
export class DeliveryEvent extends ConfirmableEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly deliveryReceipt: DeliveryEventData,
|
public readonly deliveryReceipts: ReadonlyArray<DeliveryEventData>,
|
||||||
|
public readonly envelopeId: string,
|
||||||
|
public readonly envelopeTimestamp: number,
|
||||||
confirm: ConfirmCallback
|
confirm: ConfirmCallback
|
||||||
) {
|
) {
|
||||||
super('delivery', confirm);
|
super('delivery', confirm);
|
||||||
|
@ -245,9 +248,7 @@ export class MessageEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReadOrViewEventData = Readonly<{
|
export type ReadOrViewEventData = Readonly<{
|
||||||
envelopeId: string;
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
envelopeTimestamp: number;
|
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceServiceId?: ServiceIdString;
|
sourceServiceId?: ServiceIdString;
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
|
@ -256,7 +257,9 @@ export type ReadOrViewEventData = Readonly<{
|
||||||
|
|
||||||
export class ReadEvent extends ConfirmableEvent {
|
export class ReadEvent extends ConfirmableEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly receipt: ReadOrViewEventData,
|
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
|
||||||
|
public readonly envelopeId: string,
|
||||||
|
public readonly envelopeTimestamp: number,
|
||||||
confirm: ConfirmCallback
|
confirm: ConfirmCallback
|
||||||
) {
|
) {
|
||||||
super('read', confirm);
|
super('read', confirm);
|
||||||
|
@ -265,7 +268,9 @@ export class ReadEvent extends ConfirmableEvent {
|
||||||
|
|
||||||
export class ViewEvent extends ConfirmableEvent {
|
export class ViewEvent extends ConfirmableEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly receipt: ReadOrViewEventData,
|
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
|
||||||
|
public readonly envelopeId: string,
|
||||||
|
public readonly envelopeTimestamp: number,
|
||||||
confirm: ConfirmCallback
|
confirm: ConfirmCallback
|
||||||
) {
|
) {
|
||||||
super('view', confirm);
|
super('view', confirm);
|
||||||
|
@ -405,7 +410,9 @@ export type ReadSyncEventData = Readonly<{
|
||||||
|
|
||||||
export class ReadSyncEvent extends ConfirmableEvent {
|
export class ReadSyncEvent extends ConfirmableEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly read: ReadSyncEventData,
|
public readonly reads: ReadonlyArray<ReadSyncEventData>,
|
||||||
|
public readonly envelopeId: string,
|
||||||
|
public readonly envelopeTimestamp: number,
|
||||||
confirm: ConfirmCallback
|
confirm: ConfirmCallback
|
||||||
) {
|
) {
|
||||||
super('readSync', confirm);
|
super('readSync', confirm);
|
||||||
|
@ -413,16 +420,16 @@ export class ReadSyncEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewSyncEventData = Readonly<{
|
export type ViewSyncEventData = Readonly<{
|
||||||
envelopeId: string;
|
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
envelopeTimestamp: number;
|
|
||||||
senderE164?: string;
|
senderE164?: string;
|
||||||
senderAci?: AciString;
|
senderAci?: AciString;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class ViewSyncEvent extends ConfirmableEvent {
|
export class ViewSyncEvent extends ConfirmableEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly view: ViewSyncEventData,
|
public readonly views: ReadonlyArray<ViewSyncEventData>,
|
||||||
|
public readonly envelopeId: string,
|
||||||
|
public readonly envelopeTimestamp: number,
|
||||||
confirm: ConfirmCallback
|
confirm: ConfirmCallback
|
||||||
) {
|
) {
|
||||||
super('viewSync', confirm);
|
super('viewSync', confirm);
|
||||||
|
@ -470,15 +477,16 @@ const messageToDeleteSchema = z.union([
|
||||||
authorE164: z.string(),
|
authorE164: z.string(),
|
||||||
sentAt: z.number(),
|
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>;
|
export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
|
||||||
|
|
||||||
const conversationToDeleteSchema = z.union([
|
const conversationToDeleteSchema = z.union([
|
||||||
z.object({
|
|
||||||
type: z.literal('group').readonly(),
|
|
||||||
groupId: z.string(),
|
|
||||||
}),
|
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal('aci').readonly(),
|
type: z.literal('aci').readonly(),
|
||||||
aci: z.string().refine(isAciString),
|
aci: z.string().refine(isAciString),
|
||||||
|
@ -487,6 +495,14 @@ const conversationToDeleteSchema = z.union([
|
||||||
type: z.literal('e164').readonly(),
|
type: z.literal('e164').readonly(),
|
||||||
e164: z.string(),
|
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>;
|
export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;
|
||||||
|
|
|
@ -280,7 +280,7 @@ function shouldSyncStatus(callStatus: CallStatus) {
|
||||||
return statusToProto[callStatus] != null;
|
return statusToProto[callStatus] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProtoForCallHistory(
|
export function getProtoForCallHistory(
|
||||||
callHistory: CallHistoryDetails
|
callHistory: CallHistoryDetails
|
||||||
): Proto.SyncMessage.ICallEvent | null {
|
): Proto.SyncMessage.ICallEvent | null {
|
||||||
const event = statusToProto[callHistory.status];
|
const event = statusToProto[callHistory.status];
|
||||||
|
@ -1026,7 +1026,10 @@ async function saveCallHistory({
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
if (prevMessage != null) {
|
if (prevMessage != null) {
|
||||||
await window.Signal.Data.removeMessage(prevMessage.id);
|
await window.Signal.Data.removeMessage(prevMessage.id, {
|
||||||
|
fromSync: true,
|
||||||
|
singleProtoJobQueue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return callHistory;
|
return callHistory;
|
||||||
}
|
}
|
||||||
|
@ -1209,32 +1212,10 @@ export async function clearCallHistoryDataAndSync(): Promise<void> {
|
||||||
window.MessageCache.__DEPRECATED$unregister(messageId);
|
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');
|
log.info('clearCallHistory: Queueing sync message');
|
||||||
await singleProtoJobQueue.add({
|
await singleProtoJobQueue.add(
|
||||||
contentHint: ContentHint.RESENDABLE,
|
MessageSender.getClearCallHistoryMessage(timestamp)
|
||||||
serviceId: ourAci,
|
);
|
||||||
isSyncMessage: true,
|
|
||||||
protoBase64: Bytes.toBase64(
|
|
||||||
Proto.Content.encode(contentMessage).finish()
|
|
||||||
),
|
|
||||||
type: 'callLogEventSync',
|
|
||||||
urgent: false,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('clearCallHistory: Failed to clear call history', error);
|
log.error('clearCallHistory: Failed to clear call history', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,67 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 type { MessageAttributesType } from '../model-types.d';
|
||||||
import { deletePackReference } from '../types/Stickers';
|
import { deletePackReference } from '../types/Stickers';
|
||||||
import { isStory } from '../messages/helpers';
|
import { isStory } from '../messages/helpers';
|
||||||
import { isDirectConversation } from './whatTypeOfConversation';
|
import { isDirectConversation } from './whatTypeOfConversation';
|
||||||
import * as log from '../logging/log';
|
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(
|
export async function cleanupMessages(
|
||||||
message: MessageAttributesType
|
messages: ReadonlyArray<MessageAttributesType>,
|
||||||
|
{
|
||||||
|
fromSync,
|
||||||
|
markCallHistoryDeleted,
|
||||||
|
singleProtoJobQueue,
|
||||||
|
}: {
|
||||||
|
fromSync?: boolean;
|
||||||
|
markCallHistoryDeleted: (callId: string) => Promise<void>;
|
||||||
|
singleProtoJobQueue: SingleProtoJobQueue;
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
cleanupMessageFromMemory(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 deleteMessageData(message);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await unloadedQueue.onIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removes a message from redux caches & backbone, but does NOT delete files on disk,
|
/** 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);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,9 @@ import type {
|
||||||
ConversationToDelete,
|
ConversationToDelete,
|
||||||
MessageToDelete,
|
MessageToDelete,
|
||||||
} from '../textsecure/messageReceiverEvents';
|
} 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 {
|
const {
|
||||||
getMessagesBySentAt,
|
getMessagesBySentAt,
|
||||||
|
@ -48,12 +50,16 @@ export function doesMessageMatch({
|
||||||
const conversationMatches = message.conversationId === conversationId;
|
const conversationMatches = message.conversationId === conversationId;
|
||||||
const aciMatches =
|
const aciMatches =
|
||||||
query.authorAci && author?.attributes.serviceId === query.authorAci;
|
query.authorAci && author?.attributes.serviceId === query.authorAci;
|
||||||
|
const pniMatches =
|
||||||
|
query.authorPni && author?.attributes.serviceId === query.authorPni;
|
||||||
const e164Matches =
|
const e164Matches =
|
||||||
query.authorE164 && author?.attributes.e164 === query.authorE164;
|
query.authorE164 && author?.attributes.e164 === query.authorE164;
|
||||||
const timestampMatches = sentTimestamps.has(query.sentAt);
|
const timestampMatches = sentTimestamps.has(query.sentAt);
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
conversationMatches && timestampMatches && (aciMatches || e164Matches)
|
conversationMatches &&
|
||||||
|
timestampMatches &&
|
||||||
|
(aciMatches || e164Matches || pniMatches)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +97,10 @@ export async function deleteMessage(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteAndCleanup([found], logId);
|
await deleteAndCleanup([found], logId, {
|
||||||
|
fromSync: true,
|
||||||
|
singleProtoJobQueue,
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -113,8 +122,10 @@ export async function deleteConversation(
|
||||||
const { received_at: receivedAt } = newestMessage;
|
const { received_at: receivedAt } = newestMessage;
|
||||||
|
|
||||||
await removeMessagesInConversation(conversation.id, {
|
await removeMessagesInConversation(conversation.id, {
|
||||||
|
fromSync: true,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
logId: `${logId}(receivedAt=${receivedAt})`,
|
logId: `${logId}(receivedAt=${receivedAt})`,
|
||||||
|
singleProtoJobQueue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +181,9 @@ export function getConversationFromTarget(
|
||||||
if (type === 'e164') {
|
if (type === 'e164') {
|
||||||
return window.ConversationController.get(targetConversation.e164);
|
return window.ConversationController.get(targetConversation.e164);
|
||||||
}
|
}
|
||||||
|
if (type === 'pni') {
|
||||||
|
return window.ConversationController.get(targetConversation.pni);
|
||||||
|
}
|
||||||
|
|
||||||
throw missingCaseError(type);
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
|
@ -178,6 +192,7 @@ type MessageQuery = {
|
||||||
sentAt: number;
|
sentAt: number;
|
||||||
authorAci?: AciString;
|
authorAci?: AciString;
|
||||||
authorE164?: string;
|
authorE164?: string;
|
||||||
|
authorPni?: PniString;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getMessageQueryFromTarget(
|
export function getMessageQueryFromTarget(
|
||||||
|
@ -191,6 +206,13 @@ export function getMessageQueryFromTarget(
|
||||||
}
|
}
|
||||||
return { sentAt, authorAci: targetMessage.authorAci };
|
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') {
|
if (type === 'e164') {
|
||||||
return { sentAt, authorE164: targetMessage.authorE164 };
|
return { sentAt, authorE164: targetMessage.authorE164 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { calculateExpirationTimestamp } from './expirationTimer';
|
import { calculateExpirationTimestamp } from './expirationTimer';
|
||||||
import { DAY } from './durations';
|
import { DAY } from './durations';
|
||||||
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
||||||
const existingOnboardingStoryMessageIds = window.storage.get(
|
const existingOnboardingStoryMessageIds = window.storage.get(
|
||||||
|
@ -43,7 +44,9 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
||||||
|
|
||||||
log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
|
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);
|
await window.storage.put('existingOnboardingStoryMessageIds', undefined);
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||||
profileName: getProfileName(attributes),
|
profileName: getProfileName(attributes),
|
||||||
profileSharing: attributes.profileSharing,
|
profileSharing: attributes.profileSharing,
|
||||||
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
|
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
|
||||||
|
capabilities: attributes.capabilities,
|
||||||
sharingPhoneNumber: attributes.sharingPhoneNumber,
|
sharingPhoneNumber: attributes.sharingPhoneNumber,
|
||||||
publicParams: attributes.publicParams,
|
publicParams: attributes.publicParams,
|
||||||
secretParams: attributes.secretParams,
|
secretParams: attributes.secretParams,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type { ConversationAttributesType } from '../model-types.d';
|
||||||
import { hasErrors } from '../state/selectors/message';
|
import { hasErrors } from '../state/selectors/message';
|
||||||
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
|
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
|
||||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
||||||
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
|
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
@ -196,7 +196,7 @@ export async function markConversationRead(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void expiringMessagesDeletionService.update();
|
void updateExpiringMessagesService();
|
||||||
void tapToViewMessagesDeletionService.update();
|
void tapToViewMessagesDeletionService.update();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { getSourceServiceId } from '../messages/helpers';
|
||||||
import { missingCaseError } from './missingCaseError';
|
import { missingCaseError } from './missingCaseError';
|
||||||
import { reduce } from './iterables';
|
import { reduce } from './iterables';
|
||||||
import { strictAssert } from './assert';
|
import { strictAssert } from './assert';
|
||||||
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
|
||||||
export enum ModifyTargetMessageResult {
|
export enum ModifyTargetMessageResult {
|
||||||
Modified = 'Modified',
|
Modified = 'Modified',
|
||||||
|
@ -55,24 +56,28 @@ export async function modifyTargetMessage(
|
||||||
const syncDeletes = await DeletesForMe.forMessage(message.attributes);
|
const syncDeletes = await DeletesForMe.forMessage(message.attributes);
|
||||||
if (syncDeletes.length) {
|
if (syncDeletes.length) {
|
||||||
if (!isFirstRun) {
|
if (!isFirstRun) {
|
||||||
await window.Signal.Data.removeMessage(message.id);
|
await window.Signal.Data.removeMessage(message.id, {
|
||||||
|
fromSync: true,
|
||||||
|
singleProtoJobQueue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ModifyTargetMessageResult.Deleted;
|
return ModifyTargetMessageResult.Deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
|
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;
|
let sendActionType: SendActionType;
|
||||||
const receiptType = receipt.type;
|
const receiptType = receiptSync.type;
|
||||||
switch (receiptType) {
|
switch (receiptType) {
|
||||||
case MessageReceipts.MessageReceiptType.Delivery:
|
case MessageReceipts.messageReceiptTypeSchema.enum.Delivery:
|
||||||
sendActionType = SendActionType.GotDeliveryReceipt;
|
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||||
break;
|
break;
|
||||||
case MessageReceipts.MessageReceiptType.Read:
|
case MessageReceipts.messageReceiptTypeSchema.enum.Read:
|
||||||
sendActionType = SendActionType.GotReadReceipt;
|
sendActionType = SendActionType.GotReadReceipt;
|
||||||
break;
|
break;
|
||||||
case MessageReceipts.MessageReceiptType.View:
|
case MessageReceipts.messageReceiptTypeSchema.enum.View:
|
||||||
sendActionType = SendActionType.GotViewedReceipt;
|
sendActionType = SendActionType.GotViewedReceipt;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -80,10 +85,10 @@ export async function modifyTargetMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destinationConversationId: receipt.sourceConversationId,
|
destinationConversationId: receiptSync.sourceConversationId,
|
||||||
action: {
|
action: {
|
||||||
type: sendActionType,
|
type: sendActionType,
|
||||||
updatedAt: receipt.receiptTimestamp,
|
updatedAt: receiptSync.receiptTimestamp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -123,10 +128,10 @@ export async function modifyTargetMessage(
|
||||||
if (type === 'incoming') {
|
if (type === 'incoming') {
|
||||||
// In a followup (see DESKTOP-2100), we want to make `ReadSyncs#forMessage` return
|
// 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.
|
// an array, not an object. This array wrapping makes that future a bit easier.
|
||||||
const readSync = ReadSyncs.forMessage(message);
|
const maybeSingleReadSync = await ReadSyncs.forMessage(message);
|
||||||
const readSyncs = readSync ? [readSync] : [];
|
const readSyncs = maybeSingleReadSync ? [maybeSingleReadSync] : [];
|
||||||
|
|
||||||
const viewSyncs = ViewSyncs.forMessage(message);
|
const viewSyncs = await ViewSyncs.forMessage(message);
|
||||||
|
|
||||||
const isGroupStoryReply =
|
const isGroupStoryReply =
|
||||||
isGroup(conversation.attributes) && message.get('storyId');
|
isGroup(conversation.attributes) && message.get('storyId');
|
||||||
|
@ -134,8 +139,8 @@ export async function modifyTargetMessage(
|
||||||
if (readSyncs.length !== 0 || viewSyncs.length !== 0) {
|
if (readSyncs.length !== 0 || viewSyncs.length !== 0) {
|
||||||
const markReadAt = Math.min(
|
const markReadAt = Math.min(
|
||||||
Date.now(),
|
Date.now(),
|
||||||
...readSyncs.map(sync => sync.readAt),
|
...readSyncs.map(({ readSync }) => readSync.readAt),
|
||||||
...viewSyncs.map(sync => sync.viewedAt)
|
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message.get('expireTimer')) {
|
if (message.get('expireTimer')) {
|
||||||
|
@ -180,7 +185,7 @@ export async function modifyTargetMessage(
|
||||||
if (!isFirstRun && message.getPendingMarkRead()) {
|
if (!isFirstRun && message.getPendingMarkRead()) {
|
||||||
const markReadAt = message.getPendingMarkRead();
|
const markReadAt = message.getPendingMarkRead();
|
||||||
message.setPendingMarkRead(undefined);
|
message.setPendingMarkRead(undefined);
|
||||||
const newestSentAt = readSync?.timestamp;
|
const newestSentAt = maybeSingleReadSync?.readSync.timestamp;
|
||||||
|
|
||||||
// This is primarily to allow the conversation to mark all older
|
// This is primarily to allow the conversation to mark all older
|
||||||
// messages as read, as is done when we receive a read sync for
|
// 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)) {
|
if (isStory(message.attributes)) {
|
||||||
const viewSyncs = ViewSyncs.forMessage(message);
|
const viewSyncs = await ViewSyncs.forMessage(message);
|
||||||
|
|
||||||
if (viewSyncs.length !== 0) {
|
if (viewSyncs.length !== 0) {
|
||||||
message.set({
|
message.set({
|
||||||
|
@ -218,7 +223,7 @@ export async function modifyTargetMessage(
|
||||||
|
|
||||||
const markReadAt = Math.min(
|
const markReadAt = Math.min(
|
||||||
Date.now(),
|
Date.now(),
|
||||||
...viewSyncs.map(sync => sync.viewedAt)
|
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
|
||||||
);
|
);
|
||||||
message.setPendingMarkRead(
|
message.setPendingMarkRead(
|
||||||
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ZodSchema } from 'zod';
|
import type { ZodSchema } from 'zod';
|
||||||
|
|
||||||
|
import { drop } from './drop';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as DeletesForMe from '../messageModifiers/DeletesForMe';
|
import * as DeletesForMe from '../messageModifiers/DeletesForMe';
|
||||||
import {
|
import {
|
||||||
|
@ -11,18 +12,31 @@ import {
|
||||||
deleteConversationSchema,
|
deleteConversationSchema,
|
||||||
deleteLocalConversationSchema,
|
deleteLocalConversationSchema,
|
||||||
} from '../textsecure/messageReceiverEvents';
|
} from '../textsecure/messageReceiverEvents';
|
||||||
|
import {
|
||||||
|
receiptSyncTaskSchema,
|
||||||
|
onReceipt,
|
||||||
|
} from '../messageModifiers/MessageReceipts';
|
||||||
import {
|
import {
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
deleteLocalOnlyConversation,
|
deleteLocalOnlyConversation,
|
||||||
getConversationFromTarget,
|
getConversationFromTarget,
|
||||||
} from './deleteForMe';
|
} 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([
|
const syncTaskDataSchema = z.union([
|
||||||
deleteMessageSchema,
|
deleteMessageSchema,
|
||||||
deleteConversationSchema,
|
deleteConversationSchema,
|
||||||
deleteLocalConversationSchema,
|
deleteLocalConversationSchema,
|
||||||
|
receiptSyncTaskSchema,
|
||||||
|
readSyncTaskSchema,
|
||||||
|
viewSyncTaskSchema,
|
||||||
]);
|
]);
|
||||||
export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
|
export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
|
||||||
|
|
||||||
|
@ -40,6 +54,11 @@ const SCHEMAS_BY_TYPE: Record<SyncTaskData['type'], ZodSchema> = {
|
||||||
'delete-message': deleteMessageSchema,
|
'delete-message': deleteMessageSchema,
|
||||||
'delete-conversation': deleteConversationSchema,
|
'delete-conversation': deleteConversationSchema,
|
||||||
'delete-local-conversation': deleteLocalConversationSchema,
|
'delete-local-conversation': deleteLocalConversationSchema,
|
||||||
|
Delivery: receiptSyncTaskSchema,
|
||||||
|
Read: receiptSyncTaskSchema,
|
||||||
|
View: receiptSyncTaskSchema,
|
||||||
|
ReadSync: readSyncTaskSchema,
|
||||||
|
ViewSync: viewSyncTaskSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
function toLogId(task: SyncTaskType) {
|
function toLogId(task: SyncTaskType) {
|
||||||
|
@ -77,14 +96,15 @@ export async function queueSyncTasks(
|
||||||
const { data: parsed } = parseResult;
|
const { data: parsed } = parseResult;
|
||||||
|
|
||||||
if (parsed.type === 'delete-message') {
|
if (parsed.type === 'delete-message') {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
drop(
|
||||||
await DeletesForMe.onDelete({
|
DeletesForMe.onDelete({
|
||||||
conversation: parsed.conversation,
|
conversation: parsed.conversation,
|
||||||
envelopeId,
|
envelopeId,
|
||||||
message: parsed.message,
|
message: parsed.message,
|
||||||
syncTaskId: id,
|
syncTaskId: id,
|
||||||
timestamp: sentAt,
|
timestamp: sentAt,
|
||||||
});
|
})
|
||||||
|
);
|
||||||
} else if (parsed.type === 'delete-conversation') {
|
} else if (parsed.type === 'delete-conversation') {
|
||||||
const {
|
const {
|
||||||
conversation: targetConversation,
|
conversation: targetConversation,
|
||||||
|
@ -133,6 +153,39 @@ export async function queueSyncTasks(
|
||||||
log.info(`${logId}: Done; result=${result}`);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue