Move receipts and view/read syncs to new syncTasks system

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

View file

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

View file

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

View file

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

View file

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