Support unregisteredAtTimestamp in storage service

This commit is contained in:
Fedor Indutny 2022-09-19 11:47:49 -07:00 committed by GitHub
parent 6936cc1e2e
commit 62647a357f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 34 deletions

View file

@ -66,27 +66,27 @@ message StorageRecord {
message ContactRecord {
enum IdentityState {
DEFAULT = 0;
VERIFIED = 1;
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string serviceUuid = 1;
optional string serviceE164 = 2;
optional string pni = 15;
optional bytes profileKey = 3;
optional bytes identityKey = 4;
optional IdentityState identityState = 5;
optional string givenName = 6;
optional string familyName = 7;
optional string username = 8;
optional bool blocked = 9;
optional bool whitelisted = 10;
optional bool archived = 11;
optional bool markedUnread = 12;
optional uint64 mutedUntilTimestamp = 13;
optional bool hideStory = 14;
// Next ID: 16
optional string serviceUuid = 1;
optional string serviceE164 = 2;
optional string pni = 15;
optional bytes profileKey = 3;
optional bytes identityKey = 4;
optional IdentityState identityState = 5;
optional string givenName = 6;
optional string familyName = 7;
optional string username = 8;
optional bool blocked = 9;
optional bool whitelisted = 10;
optional bool archived = 11;
optional bool markedUnread = 12;
optional uint64 mutedUntilTimestamp = 13;
optional bool hideStory = 14;
optional uint64 unregisteredAtTimestamp = 16;
}
message GroupV1Record {

1
ts/model-types.d.ts vendored
View file

@ -270,6 +270,7 @@ export type ConversationAttributesType = {
customColor?: CustomColorType;
customColorId?: string;
discoveredUnregisteredAt?: number;
firstUnregisteredAt?: number;
draftChanged?: boolean;
draftAttachments?: Array<AttachmentDraftType>;
draftBodyRanges?: Array<BodyRangeType>;

View file

@ -50,7 +50,10 @@ import { getContact } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { isConversationMuted } from '../util/isConversationMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
import { isConversationUnregistered } from '../util/isConversationUnregistered';
import {
isConversationUnregistered,
isConversationUnregisteredAndStale,
} from '../util/isConversationUnregistered';
import { missingCaseError } from '../util/missingCaseError';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164';
@ -790,6 +793,10 @@ export class ConversationModel extends window.Backbone
return isConversationUnregistered(this.attributes);
}
isUnregisteredAndStale(): boolean {
return isConversationUnregisteredAndStale(this.attributes);
}
isSMSOnly(): boolean {
return isConversationSMSOnly({
...this.attributes,
@ -797,24 +804,82 @@ export class ConversationModel extends window.Backbone
});
}
setUnregistered(): void {
log.info(`Conversation ${this.idForLogging()} is now unregistered`);
setUnregistered({
timestamp = Date.now(),
fromStorageService = false,
shouldSave = true,
}: {
timestamp?: number;
fromStorageService?: boolean;
shouldSave?: boolean;
} = {}): void {
log.info(
`Conversation ${this.idForLogging()} is now unregistered, ` +
`timestamp=${timestamp}`
);
const oldFirstUnregisteredAt = this.get('firstUnregisteredAt');
this.set({
discoveredUnregisteredAt: Date.now(),
// We always keep the latest `discoveredUnregisteredAt` because if it
// was less than 6 hours ago - `isUnregistered()` has to return `false`
// and let us retry sends.
discoveredUnregisteredAt: Math.max(
this.get('discoveredUnregisteredAt') ?? timestamp,
timestamp
),
// Here we keep the oldest `firstUnregisteredAt` unless timestamp is
// coming from storage service where remote value always wins.
firstUnregisteredAt: fromStorageService
? timestamp
: Math.min(this.get('firstUnregisteredAt') ?? timestamp, timestamp),
});
window.Signal.Data.updateConversation(this.attributes);
if (shouldSave) {
window.Signal.Data.updateConversation(this.attributes);
}
if (
!fromStorageService &&
oldFirstUnregisteredAt !== this.get('firstUnregisteredAt')
) {
this.captureChange('setUnregistered');
}
}
setRegistered(): void {
if (this.get('discoveredUnregisteredAt') === undefined) {
setRegistered({
shouldSave = true,
fromStorageService = false,
}: {
shouldSave?: boolean;
fromStorageService?: boolean;
} = {}): void {
if (
this.get('discoveredUnregisteredAt') === undefined &&
this.get('firstUnregisteredAt') === undefined
) {
return;
}
const oldFirstUnregisteredAt = this.get('firstUnregisteredAt');
log.info(`Conversation ${this.idForLogging()} is registered once again`);
this.set({
discoveredUnregisteredAt: undefined,
firstUnregisteredAt: undefined,
});
window.Signal.Data.updateConversation(this.attributes);
if (shouldSave) {
window.Signal.Data.updateConversation(this.attributes);
}
if (
!fromStorageService &&
oldFirstUnregisteredAt !== this.get('firstUnregisteredAt')
) {
this.captureChange('setRegistered');
}
}
isGroupV1AndDisabled(): boolean {
@ -5090,6 +5155,7 @@ export class ConversationModel extends window.Backbone
// [X] archived
// [X] markedUnread
// [X] dontNotifyForMentionsIfMuted
// [x] firstUnregisteredAt
captureChange(logMessage: string): void {
log.info('storageService[captureChange]', logMessage, this.idForLogging());
this.set({ needsStorageServiceSync: true });

View file

@ -262,8 +262,19 @@ async function generateManifest(
continue;
}
let shouldDrop = false;
let dropReason: string | undefined;
const validationError = conversation.validate();
if (validationError) {
shouldDrop = true;
dropReason = `local validation error=${validationError}`;
} else if (conversation.isUnregisteredAndStale()) {
shouldDrop = true;
dropReason = 'unregistered and stale';
}
if (shouldDrop) {
const droppedID = conversation.get('storageID');
const droppedVersion = conversation.get('storageVersion');
if (!droppedID) {
@ -278,8 +289,8 @@ async function generateManifest(
log.warn(
`storageService.generateManifest(${version}): ` +
`skipping contact=${recordID} ` +
`due to local validation error=${validationError}`
`dropping contact=${recordID} ` +
`due to ${dropReason}`
);
conversation.unset('storageID');
deleteKeys.push(Bytes.fromBase64(droppedID));
@ -1164,10 +1175,27 @@ async function processManifest(
storageVersion,
conversation
);
log.info(
`storageService.process(${version}): localKey=${missingKey} was not ` +
'in remote manifest'
);
// Remote might have dropped this conversation already, but our value of
// `firstUnregisteredAt` is too high for us to drop it. Don't reupload it!
if (conversation.isUnregistered()) {
log.info(
`storageService.process(${version}): localKey=${missingKey} is ` +
'unregistered and not in remote manifest'
);
conversation.setUnregistered({
timestamp: Date.now() - durations.MONTH,
fromStorageService: true,
// Saving below
shouldSave: false,
});
} else {
log.info(
`storageService.process(${version}): localKey=${missingKey} ` +
'was not in remote manifest'
);
}
conversation.unset('storageID');
conversation.unset('storageVersion');
updateConversation(conversation.attributes);

View file

@ -175,6 +175,9 @@ export async function toContactRecord(
if (conversation.get('hideStory') !== undefined) {
contactRecord.hideStory = Boolean(conversation.get('hideStory'));
}
contactRecord.unregisteredAtTimestamp = getSafeLongFromTimestamp(
conversation.get('firstUnregisteredAt')
);
applyUnknownFields(contactRecord, conversation);
@ -986,6 +989,19 @@ export async function mergeContactRecord(
}
);
if (
!contactRecord.unregisteredAtTimestamp ||
contactRecord.unregisteredAtTimestamp.equals(0)
) {
conversation.setRegistered({ fromStorageService: true, shouldSave: false });
} else {
conversation.setUnregistered({
timestamp: getTimestampFromLong(contactRecord.unregisteredAtTimestamp),
fromStorageService: true,
shouldSave: false,
});
}
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
await toContactRecord(conversation),
contactRecord,

View file

@ -1,9 +1,10 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isMoreRecentThan } from './timestamp';
import { isMoreRecentThan, isOlderThan } from './timestamp';
import { HOUR, MONTH } from './durations';
const SIX_HOURS = 1000 * 60 * 60 * 6;
const SIX_HOURS = 6 * HOUR;
export function isConversationUnregistered({
discoveredUnregisteredAt,
@ -13,3 +14,11 @@ export function isConversationUnregistered({
isMoreRecentThan(discoveredUnregisteredAt, SIX_HOURS)
);
}
export function isConversationUnregisteredAndStale({
firstUnregisteredAt,
}: Readonly<{ firstUnregisteredAt?: number }>): boolean {
return Boolean(
firstUnregisteredAt && isOlderThan(firstUnregisteredAt, MONTH)
);
}