Move receipts and view/read syncs to new syncTasks system
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
949104c316
commit
b95dd1a70f
33 changed files with 1242 additions and 612 deletions
|
@ -137,6 +137,8 @@ import type {
|
|||
DeleteForMeSyncEventData,
|
||||
DeleteForMeSyncTarget,
|
||||
ConversationToDelete,
|
||||
ViewSyncEventData,
|
||||
ReadSyncEventData,
|
||||
} from './messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import * as durations from '../util/durations';
|
||||
|
@ -1728,15 +1730,17 @@ export default class MessageReceiver
|
|||
await this.dispatchAndWait(
|
||||
getEnvelopeId(envelope),
|
||||
new DeliveryEvent(
|
||||
{
|
||||
envelopeId: envelope.id,
|
||||
timestamp: envelope.timestamp,
|
||||
envelopeTimestamp: envelope.timestamp,
|
||||
source: envelope.source,
|
||||
sourceServiceId: envelope.sourceServiceId,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
wasSentEncrypted: false,
|
||||
},
|
||||
[
|
||||
{
|
||||
timestamp: envelope.timestamp,
|
||||
source: envelope.source,
|
||||
sourceServiceId: envelope.sourceServiceId,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
wasSentEncrypted: false,
|
||||
},
|
||||
],
|
||||
envelope.id,
|
||||
envelope.timestamp,
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
)
|
||||
);
|
||||
|
@ -2907,22 +2911,22 @@ export default class MessageReceiver
|
|||
|
||||
const logId = getEnvelopeId(envelope);
|
||||
|
||||
await Promise.all(
|
||||
receiptMessage.timestamp.map(async rawTimestamp => {
|
||||
const ev = new EventClass(
|
||||
{
|
||||
envelopeId: envelope.id,
|
||||
timestamp: rawTimestamp?.toNumber(),
|
||||
envelopeTimestamp: envelope.timestamp,
|
||||
source: envelope.source,
|
||||
sourceServiceId: envelope.sourceServiceId,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
wasSentEncrypted: true,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
await this.dispatchAndWait(logId, ev);
|
||||
})
|
||||
const receipts = receiptMessage.timestamp.map(rawTimestamp => ({
|
||||
timestamp: rawTimestamp?.toNumber(),
|
||||
source: envelope.source,
|
||||
sourceServiceId: envelope.sourceServiceId,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
wasSentEncrypted: true as const,
|
||||
}));
|
||||
|
||||
await this.dispatchAndWait(
|
||||
logId,
|
||||
new EventClass(
|
||||
receipts,
|
||||
envelope.id,
|
||||
envelope.timestamp,
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3469,23 +3473,27 @@ export default class MessageReceiver
|
|||
|
||||
logUnexpectedUrgentValue(envelope, 'readSync');
|
||||
|
||||
const results = [];
|
||||
for (const { timestamp, sender, senderAci } of read) {
|
||||
const ev = new ReadSyncEvent(
|
||||
{
|
||||
envelopeId: envelope.id,
|
||||
envelopeTimestamp: envelope.timestamp,
|
||||
timestamp: timestamp?.toNumber(),
|
||||
sender: dropNull(sender),
|
||||
senderAci: senderAci
|
||||
? normalizeAci(senderAci, 'handleRead.senderAci')
|
||||
: undefined,
|
||||
},
|
||||
const reads = read.map(
|
||||
({ timestamp, sender, senderAci }): ReadSyncEventData => ({
|
||||
envelopeId: envelope.id,
|
||||
envelopeTimestamp: envelope.timestamp,
|
||||
timestamp: timestamp?.toNumber(),
|
||||
sender: dropNull(sender),
|
||||
senderAci: senderAci
|
||||
? normalizeAci(senderAci, 'handleRead.senderAci')
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
await this.dispatchAndWait(
|
||||
logId,
|
||||
new ReadSyncEvent(
|
||||
reads,
|
||||
envelope.id,
|
||||
envelope.timestamp,
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
results.push(this.dispatchAndWait(logId, ev));
|
||||
}
|
||||
await Promise.all(results);
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async handleViewed(
|
||||
|
@ -3497,23 +3505,25 @@ export default class MessageReceiver
|
|||
|
||||
logUnexpectedUrgentValue(envelope, 'viewSync');
|
||||
|
||||
await Promise.all(
|
||||
viewed.map(async ({ timestamp, senderE164, senderAci }) => {
|
||||
const ev = new ViewSyncEvent(
|
||||
{
|
||||
envelopeId: envelope.id,
|
||||
envelopeTimestamp: envelope.timestamp,
|
||||
timestamp: timestamp?.toNumber(),
|
||||
senderE164: dropNull(senderE164),
|
||||
senderAci: senderAci
|
||||
? normalizeAci(senderAci, 'handleViewed.senderAci')
|
||||
: undefined,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
await this.dispatchAndWait(logId, ev);
|
||||
const views = viewed.map(
|
||||
({ timestamp, senderE164, senderAci }): ViewSyncEventData => ({
|
||||
timestamp: timestamp?.toNumber(),
|
||||
senderE164: dropNull(senderE164),
|
||||
senderAci: senderAci
|
||||
? normalizeAci(senderAci, 'handleViewed.senderAci')
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
await this.dispatchAndWait(
|
||||
logId,
|
||||
new ViewSyncEvent(
|
||||
views,
|
||||
envelope.id,
|
||||
envelope.timestamp,
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async handleCallEvent(
|
||||
|
@ -3663,19 +3673,28 @@ export default class MessageReceiver
|
|||
? processConversationToDelete(item.conversation, logId)
|
||||
: undefined;
|
||||
|
||||
if (messages?.length && conversation) {
|
||||
// We want each message in its own task
|
||||
return messages.map(innerItem => {
|
||||
return {
|
||||
type: 'delete-message' as const,
|
||||
message: innerItem,
|
||||
conversation,
|
||||
timestamp,
|
||||
};
|
||||
});
|
||||
if (!conversation) {
|
||||
log.warn(
|
||||
`${logId}/handleDeleteForMeSync/messageDeletes: No target conversation`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (!messages?.length) {
|
||||
log.warn(
|
||||
`${logId}/handleDeleteForMeSync/messageDeletes: No target messages`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// We want each message in its own task
|
||||
return messages.map(innerItem => {
|
||||
return {
|
||||
type: 'delete-message' as const,
|
||||
message: innerItem,
|
||||
conversation,
|
||||
timestamp,
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
|
@ -3692,17 +3711,26 @@ export default class MessageReceiver
|
|||
? processConversationToDelete(item.conversation, logId)
|
||||
: undefined;
|
||||
|
||||
if (mostRecentMessages?.length && conversation) {
|
||||
return {
|
||||
type: 'delete-conversation' as const,
|
||||
conversation,
|
||||
isFullDelete: Boolean(item.isFullDelete),
|
||||
mostRecentMessages,
|
||||
timestamp,
|
||||
};
|
||||
if (!conversation) {
|
||||
log.warn(
|
||||
`${logId}/handleDeleteForMeSync/conversationDeletes: No target conversation`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (!mostRecentMessages?.length) {
|
||||
log.warn(
|
||||
`${logId}/handleDeleteForMeSync/conversationDeletes: No target messages`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return {
|
||||
type: 'delete-conversation' as const,
|
||||
conversation,
|
||||
isFullDelete: Boolean(item.isFullDelete),
|
||||
mostRecentMessages,
|
||||
timestamp,
|
||||
};
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
|
@ -3716,15 +3744,18 @@ export default class MessageReceiver
|
|||
? processConversationToDelete(item.conversation, logId)
|
||||
: undefined;
|
||||
|
||||
if (conversation) {
|
||||
return {
|
||||
type: 'delete-local-conversation' as const,
|
||||
conversation,
|
||||
timestamp,
|
||||
};
|
||||
if (!conversation) {
|
||||
log.warn(
|
||||
`${logId}/handleDeleteForMeSync/localOnlyConversationDeletes: No target conversation`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return {
|
||||
type: 'delete-local-conversation' as const,
|
||||
conversation,
|
||||
timestamp,
|
||||
};
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
|
@ -3969,15 +4000,32 @@ function processMessageToDelete(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (target.authorAci) {
|
||||
return {
|
||||
type: 'aci' as const,
|
||||
authorAci: normalizeAci(
|
||||
target.authorAci,
|
||||
`${logId}/processMessageToDelete`
|
||||
),
|
||||
sentAt,
|
||||
};
|
||||
const { authorServiceId } = target;
|
||||
if (authorServiceId) {
|
||||
if (isAciString(authorServiceId)) {
|
||||
return {
|
||||
type: 'aci' as const,
|
||||
authorAci: normalizeAci(
|
||||
authorServiceId,
|
||||
`${logId}/processMessageToDelete/aci`
|
||||
),
|
||||
sentAt,
|
||||
};
|
||||
}
|
||||
if (isPniString(authorServiceId)) {
|
||||
return {
|
||||
type: 'pni' as const,
|
||||
authorPni: normalizePni(
|
||||
authorServiceId,
|
||||
`${logId}/processMessageToDelete/pni`
|
||||
),
|
||||
sentAt,
|
||||
};
|
||||
}
|
||||
log.error(
|
||||
`${logId}/processMessageToDelete: invalid authorServiceId, Dropping AddressableMessage.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (target.authorE164) {
|
||||
return {
|
||||
|
@ -3997,13 +4045,25 @@ function processConversationToDelete(
|
|||
target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
|
||||
logId: string
|
||||
): ConversationToDelete | undefined {
|
||||
const { threadAci, threadGroupId, threadE164 } = target;
|
||||
const { threadServiceId, threadGroupId, threadE164 } = target;
|
||||
|
||||
if (threadAci) {
|
||||
return {
|
||||
type: 'aci' as const,
|
||||
aci: normalizeAci(threadAci, `${logId}/threadAci`),
|
||||
};
|
||||
if (threadServiceId) {
|
||||
if (isAciString(threadServiceId)) {
|
||||
return {
|
||||
type: 'aci' as const,
|
||||
aci: normalizeAci(threadServiceId, `${logId}/aci`),
|
||||
};
|
||||
}
|
||||
if (isPniString(threadServiceId)) {
|
||||
return {
|
||||
type: 'pni' as const,
|
||||
pni: normalizePni(threadServiceId, `${logId}/pni`),
|
||||
};
|
||||
}
|
||||
log.error(
|
||||
`${logId}/processConversationToDelete: Invalid threadServiceId, dropping ConversationIdentifier.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (threadGroupId) {
|
||||
return {
|
||||
|
|
|
@ -89,6 +89,14 @@ import type {
|
|||
MessageToDelete,
|
||||
} from './messageReceiverEvents';
|
||||
import { getConversationFromTarget } from '../util/deleteForMe';
|
||||
import type { CallDetails } from '../types/CallDisposition';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../types/CallDisposition';
|
||||
import { getProtoForCallHistory } from '../util/callDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
|
||||
export type SendMetadataType = {
|
||||
[serviceId: ServiceIdString]: {
|
||||
|
@ -1567,6 +1575,71 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
static getClearCallHistoryMessage(timestamp: number): SingleProtoJobData {
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
const callLogEvent = new Proto.SyncMessage.CallLogEvent({
|
||||
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
});
|
||||
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.callLogEvent = callLogEvent;
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return {
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
serviceId: ourAci,
|
||||
isSyncMessage: true,
|
||||
protoBase64: Bytes.toBase64(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
type: 'callLogEventSync',
|
||||
urgent: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDeleteCallEvent(callDetails: CallDetails): SingleProtoJobData {
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
const { mode } = callDetails;
|
||||
let status;
|
||||
if (mode === CallMode.Adhoc) {
|
||||
status = AdhocCallStatus.Deleted;
|
||||
} else if (mode === CallMode.Direct) {
|
||||
status = DirectCallStatus.Deleted;
|
||||
} else if (mode === CallMode.Group) {
|
||||
status = GroupCallStatus.Deleted;
|
||||
} else {
|
||||
throw missingCaseError(mode);
|
||||
}
|
||||
const callEvent = getProtoForCallHistory({
|
||||
...callDetails,
|
||||
status,
|
||||
});
|
||||
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.callEvent = callEvent;
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return {
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
serviceId: ourAci,
|
||||
isSyncMessage: true,
|
||||
protoBase64: Bytes.toBase64(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
type: 'callLogEventSync',
|
||||
urgent: false,
|
||||
};
|
||||
}
|
||||
|
||||
async syncReadMessages(
|
||||
reads: ReadonlyArray<{
|
||||
senderAci?: AciString;
|
||||
|
@ -2353,9 +2426,11 @@ function toAddressableMessage(message: MessageToDelete) {
|
|||
targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
|
||||
|
||||
if (message.type === 'aci') {
|
||||
targetMessage.authorAci = message.authorAci;
|
||||
targetMessage.authorServiceId = message.authorAci;
|
||||
} else if (message.type === 'e164') {
|
||||
targetMessage.authorE164 = message.authorE164;
|
||||
} else if (message.type === 'pni') {
|
||||
targetMessage.authorServiceId = message.authorPni;
|
||||
} else {
|
||||
throw missingCaseError(message);
|
||||
}
|
||||
|
@ -2368,7 +2443,9 @@ function toConversationIdentifier(conversation: ConversationToDelete) {
|
|||
new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
|
||||
|
||||
if (conversation.type === 'aci') {
|
||||
targetConversation.threadAci = conversation.aci;
|
||||
targetConversation.threadServiceId = conversation.aci;
|
||||
} else if (conversation.type === 'pni') {
|
||||
targetConversation.threadServiceId = conversation.pni;
|
||||
} else if (conversation.type === 'group') {
|
||||
targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
|
||||
} else if (conversation.type === 'e164') {
|
||||
|
|
|
@ -703,8 +703,12 @@ export type WebAPIConnectType = {
|
|||
connect: (options: WebAPIConnectOptionsType) => WebAPIType;
|
||||
};
|
||||
|
||||
export type CapabilitiesType = Record<string, never>;
|
||||
export type CapabilitiesUploadType = Record<string, never>;
|
||||
export type CapabilitiesType = {
|
||||
deleteSync: boolean;
|
||||
};
|
||||
export type CapabilitiesUploadType = {
|
||||
deleteSync: true;
|
||||
};
|
||||
|
||||
type StickerPackManifestType = Uint8Array;
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@ import type { PublicKey } from '@signalapp/libsignal-client';
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { SignalService as Proto } from '../protobuf';
|
||||
import type { ServiceIdString, AciString } from '../types/ServiceId';
|
||||
import {
|
||||
type ServiceIdString,
|
||||
type AciString,
|
||||
isPniString,
|
||||
} from '../types/ServiceId';
|
||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
|
||||
import type {
|
||||
ProcessedEnvelope,
|
||||
|
@ -93,7 +97,6 @@ export class EnvelopeUnsealedEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
// Emitted when we queue previously-decrypted events from the cache
|
||||
export class EnvelopeQueuedEvent extends Event {
|
||||
constructor(public readonly envelope: ProcessedEnvelope) {
|
||||
super('envelopeQueued');
|
||||
|
@ -113,9 +116,7 @@ export class ConfirmableEvent extends Event {
|
|||
}
|
||||
|
||||
export type DeliveryEventData = Readonly<{
|
||||
envelopeId: string;
|
||||
timestamp: number;
|
||||
envelopeTimestamp: number;
|
||||
source?: string;
|
||||
sourceServiceId?: ServiceIdString;
|
||||
sourceDevice?: number;
|
||||
|
@ -124,7 +125,9 @@ export type DeliveryEventData = Readonly<{
|
|||
|
||||
export class DeliveryEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly deliveryReceipt: DeliveryEventData,
|
||||
public readonly deliveryReceipts: ReadonlyArray<DeliveryEventData>,
|
||||
public readonly envelopeId: string,
|
||||
public readonly envelopeTimestamp: number,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('delivery', confirm);
|
||||
|
@ -245,9 +248,7 @@ export class MessageEvent extends ConfirmableEvent {
|
|||
}
|
||||
|
||||
export type ReadOrViewEventData = Readonly<{
|
||||
envelopeId: string;
|
||||
timestamp: number;
|
||||
envelopeTimestamp: number;
|
||||
source?: string;
|
||||
sourceServiceId?: ServiceIdString;
|
||||
sourceDevice?: number;
|
||||
|
@ -256,7 +257,9 @@ export type ReadOrViewEventData = Readonly<{
|
|||
|
||||
export class ReadEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly receipt: ReadOrViewEventData,
|
||||
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
|
||||
public readonly envelopeId: string,
|
||||
public readonly envelopeTimestamp: number,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('read', confirm);
|
||||
|
@ -265,7 +268,9 @@ export class ReadEvent extends ConfirmableEvent {
|
|||
|
||||
export class ViewEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly receipt: ReadOrViewEventData,
|
||||
public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
|
||||
public readonly envelopeId: string,
|
||||
public readonly envelopeTimestamp: number,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('view', confirm);
|
||||
|
@ -405,7 +410,9 @@ export type ReadSyncEventData = Readonly<{
|
|||
|
||||
export class ReadSyncEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly read: ReadSyncEventData,
|
||||
public readonly reads: ReadonlyArray<ReadSyncEventData>,
|
||||
public readonly envelopeId: string,
|
||||
public readonly envelopeTimestamp: number,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('readSync', confirm);
|
||||
|
@ -413,16 +420,16 @@ export class ReadSyncEvent extends ConfirmableEvent {
|
|||
}
|
||||
|
||||
export type ViewSyncEventData = Readonly<{
|
||||
envelopeId: string;
|
||||
timestamp?: number;
|
||||
envelopeTimestamp: number;
|
||||
senderE164?: string;
|
||||
senderAci?: AciString;
|
||||
}>;
|
||||
|
||||
export class ViewSyncEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly view: ViewSyncEventData,
|
||||
public readonly views: ReadonlyArray<ViewSyncEventData>,
|
||||
public readonly envelopeId: string,
|
||||
public readonly envelopeTimestamp: number,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('viewSync', confirm);
|
||||
|
@ -470,15 +477,16 @@ const messageToDeleteSchema = z.union([
|
|||
authorE164: z.string(),
|
||||
sentAt: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('pni').readonly(),
|
||||
authorPni: z.string().refine(isPniString),
|
||||
sentAt: z.number(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
|
||||
|
||||
const conversationToDeleteSchema = z.union([
|
||||
z.object({
|
||||
type: z.literal('group').readonly(),
|
||||
groupId: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('aci').readonly(),
|
||||
aci: z.string().refine(isAciString),
|
||||
|
@ -487,6 +495,14 @@ const conversationToDeleteSchema = z.union([
|
|||
type: z.literal('e164').readonly(),
|
||||
e164: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('group').readonly(),
|
||||
groupId: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('pni').readonly(),
|
||||
pni: z.string().refine(isPniString),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue