Update backup import/export with new SendStatus, FilePointer, and GroupSnapshot updates

This commit is contained in:
trevor-signal 2024-08-19 20:47:02 -04:00 committed by GitHub
parent f44a16489c
commit 301f7a505a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 239 additions and 119 deletions

View file

@ -86,7 +86,6 @@ import {
import * as Bytes from '../../Bytes';
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
import { SendStatus } from '../../messages/MessageSendState';
import { deriveGroupFields } from '../../groups';
import { BACKUP_VERSION } from './constants';
import { getMessageIdForLogging } from '../../util/idForLogging';
import { getCallsHistoryForRedux } from '../callHistoryLoader';
@ -811,20 +810,12 @@ export class BackupExportStream extends Readable {
const masterKey = Bytes.fromBase64(convo.masterKey);
let publicKey;
if (convo.publicParams) {
publicKey = Bytes.fromBase64(convo.publicParams);
} else {
({ publicParams: publicKey } = deriveGroupFields(masterKey));
}
res.group = {
masterKey,
whitelisted: convo.profileSharing,
hideStory: convo.hideStory === true,
storySendMode,
snapshot: {
publicKey,
title: {
title: convo.name ?? '',
},
@ -2204,8 +2195,6 @@ export class BackupExportStream extends Readable {
'sendStateByConversationId' | 'unidentifiedDeliveries' | 'errors'
>
): Backups.ChatItem.IOutgoingMessageDetails {
const BackupSendStatus = Backups.SendStatus.Status;
const sealedSenderServiceIds = new Set(unidentifiedDeliveries);
const errorMap = new Map(
errors.map(({ serviceId, name }) => {
@ -2213,65 +2202,78 @@ export class BackupExportStream extends Readable {
})
);
const sendStatus = new Array<Backups.ISendStatus>();
const sendStatuses = new Array<Backups.ISendStatus>();
for (const [id, entry] of Object.entries(sendStateByConversationId)) {
const target = window.ConversationController.get(id);
if (!target) {
log.warn(`backups: no send target for a message ${sentAt}`);
continue;
}
const { serviceId } = target.attributes;
const recipientId = this.getOrPushPrivateRecipient(target.attributes);
const timestamp =
entry.updatedAt != null
? getSafeLongFromTimestamp(entry.updatedAt)
: null;
const sendStatus = new Backups.SendStatus({ recipientId, timestamp });
const sealedSender = serviceId
? sealedSenderServiceIds.has(serviceId)
: false;
let deliveryStatus: Backups.SendStatus.Status;
switch (entry.status) {
case SendStatus.Pending:
deliveryStatus = BackupSendStatus.PENDING;
sendStatus.pending = new Backups.SendStatus.Pending();
break;
case SendStatus.Sent:
deliveryStatus = BackupSendStatus.SENT;
sendStatus.sent = new Backups.SendStatus.Sent({
sealedSender,
});
break;
case SendStatus.Delivered:
deliveryStatus = BackupSendStatus.DELIVERED;
sendStatus.delivered = new Backups.SendStatus.Delivered({
sealedSender,
});
break;
case SendStatus.Read:
deliveryStatus = BackupSendStatus.READ;
sendStatus.read = new Backups.SendStatus.Read({
sealedSender,
});
break;
case SendStatus.Viewed:
deliveryStatus = BackupSendStatus.VIEWED;
sendStatus.viewed = new Backups.SendStatus.Viewed({
sealedSender,
});
break;
case SendStatus.Failed:
deliveryStatus = BackupSendStatus.FAILED;
case SendStatus.Failed: {
sendStatus.failed = new Backups.SendStatus.Failed();
if (!serviceId) {
break;
}
const errorName = errorMap.get(serviceId);
if (!errorName) {
break;
}
const identityKeyMismatch = errorName === 'OutgoingIdentityKeyError';
if (identityKeyMismatch) {
sendStatus.failed.reason =
Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH;
} else {
sendStatus.failed.reason =
Backups.SendStatus.Failed.FailureReason.NETWORK;
}
break;
}
default:
throw missingCaseError(entry.status);
}
const { serviceId } = target.attributes;
let networkFailure = false;
let identityKeyMismatch = false;
let sealedSender = false;
if (serviceId) {
const errorName = errorMap.get(serviceId);
if (errorName !== undefined) {
identityKeyMismatch = errorName === 'OutgoingIdentityKeyError';
networkFailure = !identityKeyMismatch;
}
sealedSender = sealedSenderServiceIds.has(serviceId);
}
sendStatus.push({
recipientId: this.getOrPushPrivateRecipient(target.attributes),
lastStatusUpdateTimestamp:
entry.updatedAt != null
? getSafeLongFromTimestamp(entry.updatedAt)
: null,
deliveryStatus,
networkFailure,
identityKeyMismatch,
sealedSender,
});
sendStatuses.push(sendStatus);
}
return {
sendStatus,
sendStatus: sendStatuses,
};
}

View file

@ -1278,8 +1278,6 @@ export class BackupImportStream extends Writable {
if (outgoing) {
const sendStateByConversationId: SendStateByConversationId = {};
const BackupSendStatus = Backups.SendStatus.Status;
const unidentifiedDeliveries = new Array<ServiceIdString>();
const errors = new Array<CustomError>();
for (const status of outgoing.sendStatus ?? []) {
@ -1295,57 +1293,71 @@ export class BackupImportStream extends Writable {
'status target conversation not found'
);
let sendStatus: SendStatus;
switch (status.deliveryStatus) {
case BackupSendStatus.PENDING:
sendStatus = SendStatus.Pending;
break;
case BackupSendStatus.SENT:
sendStatus = SendStatus.Sent;
break;
case BackupSendStatus.DELIVERED:
sendStatus = SendStatus.Delivered;
break;
case BackupSendStatus.READ:
sendStatus = SendStatus.Read;
break;
case BackupSendStatus.VIEWED:
sendStatus = SendStatus.Viewed;
break;
case BackupSendStatus.FAILED:
default:
sendStatus = SendStatus.Failed;
break;
// Desktop does not keep track of users we did not attempt to send to
if (status.skipped) {
continue;
}
const { serviceId } = target;
if (target.serviceId) {
if (status.sealedSender) {
unidentifiedDeliveries.push(target.serviceId);
let sendStatus: SendStatus;
if (status.pending) {
sendStatus = SendStatus.Pending;
} else if (status.sent) {
sendStatus = SendStatus.Sent;
if (serviceId && status.sent.sealedSender) {
unidentifiedDeliveries.push(serviceId);
}
if (status.identityKeyMismatch) {
errors.push({
serviceId: target.serviceId,
name: 'OutgoingIdentityKeyError',
// See: ts/textsecure/Errors
message: `The identity of ${target.serviceId} has changed.`,
});
} else if (status.networkFailure) {
errors.push({
serviceId: target.serviceId,
name: 'OutgoingMessageError',
// See: ts/textsecure/Errors
message: 'no http error',
});
} else if (status.delivered) {
sendStatus = SendStatus.Delivered;
if (serviceId && status.delivered.sealedSender) {
unidentifiedDeliveries.push(serviceId);
}
} else if (status.read) {
sendStatus = SendStatus.Read;
if (serviceId && status.read.sealedSender) {
unidentifiedDeliveries.push(serviceId);
}
} else if (status.viewed) {
sendStatus = SendStatus.Viewed;
if (serviceId && status.viewed.sealedSender) {
unidentifiedDeliveries.push(serviceId);
}
} else if (status.failed) {
sendStatus = SendStatus.Failed;
strictAssert(
status.failed.reason != null,
'Failure reason must exist'
);
switch (status.failed.reason) {
case Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH:
errors.push({
serviceId,
name: 'OutgoingIdentityKeyError',
// See: ts/textsecure/Errors
message: `The identity of ${serviceId} has changed.`,
});
break;
case Backups.SendStatus.Failed.FailureReason.NETWORK:
case Backups.SendStatus.Failed.FailureReason.UNKNOWN:
errors.push({
serviceId,
name: 'OutgoingMessageError',
// See: ts/textsecure/Errors
message: 'no http error',
});
break;
default:
throw missingCaseError(status.failed.reason);
}
} else {
throw new Error(`Unknown sendStatus received: ${status}`);
}
sendStateByConversationId[target.id] = {
status: sendStatus,
updatedAt:
status.lastStatusUpdateTimestamp != null &&
!status.lastStatusUpdateTimestamp.isZero()
? getTimestampFromLong(status.lastStatusUpdateTimestamp)
status.timestamp != null && !status.timestamp.isZero()
? getTimestampFromLong(status.timestamp)
: undefined,
};
}

View file

@ -105,7 +105,7 @@ export function convertFilePointerToAttachment(
cdnNumber: transitCdnNumber ?? undefined,
key: key?.length ? Bytes.toBase64(key) : undefined,
digest: digest?.length ? Bytes.toBase64(digest) : undefined,
size: size?.toNumber() ?? 0,
size: size ?? 0,
backupLocator: mediaName
? {
mediaName,
@ -401,7 +401,7 @@ function getBackupLocator(attachment: AttachmentDownloadableFromBackupTier) {
cdnNumber: attachment.backupLocator.cdnNumber,
digest: Bytes.fromBase64(attachment.digest),
key: Bytes.fromBase64(attachment.key),
size: Long.fromNumber(attachment.size),
size: attachment.size,
transitCdnKey: attachment.cdnKey,
transitCdnNumber: attachment.cdnNumber,
});

View file

@ -24,6 +24,7 @@ import {
} from './helpers';
const CONTACT_A = generateAci();
const CONTACT_B = generateAci();
const GV1_ID = Bytes.toBinary(getRandomBytes(ID_V1_LENGTH));
const BADGE_RECEIPT =
@ -37,6 +38,7 @@ const BADGE_RECEIPT =
describe('backup/bubble messages', () => {
let contactA: ConversationModel;
let contactB: ConversationModel;
let gv1: ConversationModel;
beforeEach(async () => {
@ -51,6 +53,11 @@ describe('backup/bubble messages', () => {
'private',
{ systemGivenName: 'CONTACT_A' }
);
contactB = await window.ConversationController.getOrCreateAndWait(
CONTACT_B,
'private',
{ systemGivenName: 'CONTACT_B' }
);
gv1 = await window.ConversationController.getOrCreateAndWait(
GV1_ID,
@ -346,12 +353,15 @@ describe('backup/bubble messages', () => {
[contactA.id]: {
status: SendStatus.Delivered,
},
[contactB.id]: {
status: SendStatus.Failed,
},
},
errors: [
{
serviceId: CONTACT_A,
serviceId: CONTACT_B,
name: 'OutgoingIdentityKeyError',
message: `The identity of ${CONTACT_A} has changed.`,
message: `The identity of ${CONTACT_B} has changed.`,
},
],
timestamp: 3,
@ -367,6 +377,9 @@ describe('backup/bubble messages', () => {
sourceServiceId: OUR_ACI,
sendStateByConversationId: {
[contactA.id]: {
status: SendStatus.Failed,
},
[contactB.id]: {
status: SendStatus.Delivered,
},
},

View file

@ -75,7 +75,7 @@ describe('convertFilePointerToAttachment', () => {
backupLocator: new Backups.FilePointer.BackupLocator({
mediaName: 'mediaName',
cdnNumber: 3,
size: Long.fromNumber(128),
size: 128,
key: Bytes.fromString('key'),
digest: Bytes.fromString('digest'),
transitCdnKey: 'transitCdnKey',
@ -215,7 +215,7 @@ const defaultBackupLocator = new Backups.FilePointer.BackupLocator({
cdnNumber: null,
key: Bytes.fromBase64('key'),
digest: defaultDigest,
size: Long.fromNumber(100),
size: 100,
transitCdnKey: 'cdnKey',
transitCdnNumber: 2,
});