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

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

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

View file

@ -50,7 +50,10 @@ import { getContact } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isConversationMuted } from '../util/isConversationMuted'; import { isConversationMuted } from '../util/isConversationMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly'; import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
import { isConversationUnregistered } from '../util/isConversationUnregistered'; import {
isConversationUnregistered,
isConversationUnregisteredAndStale,
} from '../util/isConversationUnregistered';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { sniffImageMimeType } from '../util/sniffImageMimeType'; import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164'; import { isValidE164 } from '../util/isValidE164';
@ -790,6 +793,10 @@ export class ConversationModel extends window.Backbone
return isConversationUnregistered(this.attributes); return isConversationUnregistered(this.attributes);
} }
isUnregisteredAndStale(): boolean {
return isConversationUnregisteredAndStale(this.attributes);
}
isSMSOnly(): boolean { isSMSOnly(): boolean {
return isConversationSMSOnly({ return isConversationSMSOnly({
...this.attributes, ...this.attributes,
@ -797,24 +804,82 @@ export class ConversationModel extends window.Backbone
}); });
} }
setUnregistered(): void { setUnregistered({
log.info(`Conversation ${this.idForLogging()} is now unregistered`); 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({ 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 { setRegistered({
if (this.get('discoveredUnregisteredAt') === undefined) { shouldSave = true,
fromStorageService = false,
}: {
shouldSave?: boolean;
fromStorageService?: boolean;
} = {}): void {
if (
this.get('discoveredUnregisteredAt') === undefined &&
this.get('firstUnregisteredAt') === undefined
) {
return; return;
} }
const oldFirstUnregisteredAt = this.get('firstUnregisteredAt');
log.info(`Conversation ${this.idForLogging()} is registered once again`); log.info(`Conversation ${this.idForLogging()} is registered once again`);
this.set({ this.set({
discoveredUnregisteredAt: undefined, 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 { isGroupV1AndDisabled(): boolean {
@ -5090,6 +5155,7 @@ export class ConversationModel extends window.Backbone
// [X] archived // [X] archived
// [X] markedUnread // [X] markedUnread
// [X] dontNotifyForMentionsIfMuted // [X] dontNotifyForMentionsIfMuted
// [x] firstUnregisteredAt
captureChange(logMessage: string): void { captureChange(logMessage: string): void {
log.info('storageService[captureChange]', logMessage, this.idForLogging()); log.info('storageService[captureChange]', logMessage, this.idForLogging());
this.set({ needsStorageServiceSync: true }); this.set({ needsStorageServiceSync: true });

View file

@ -262,8 +262,19 @@ async function generateManifest(
continue; continue;
} }
let shouldDrop = false;
let dropReason: string | undefined;
const validationError = conversation.validate(); const validationError = conversation.validate();
if (validationError) { 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 droppedID = conversation.get('storageID');
const droppedVersion = conversation.get('storageVersion'); const droppedVersion = conversation.get('storageVersion');
if (!droppedID) { if (!droppedID) {
@ -278,8 +289,8 @@ async function generateManifest(
log.warn( log.warn(
`storageService.generateManifest(${version}): ` + `storageService.generateManifest(${version}): ` +
`skipping contact=${recordID} ` + `dropping contact=${recordID} ` +
`due to local validation error=${validationError}` `due to ${dropReason}`
); );
conversation.unset('storageID'); conversation.unset('storageID');
deleteKeys.push(Bytes.fromBase64(droppedID)); deleteKeys.push(Bytes.fromBase64(droppedID));
@ -1164,10 +1175,27 @@ async function processManifest(
storageVersion, storageVersion,
conversation conversation
); );
log.info(
`storageService.process(${version}): localKey=${missingKey} was not ` + // Remote might have dropped this conversation already, but our value of
'in remote manifest' // `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('storageID');
conversation.unset('storageVersion'); conversation.unset('storageVersion');
updateConversation(conversation.attributes); updateConversation(conversation.attributes);

View file

@ -175,6 +175,9 @@ export async function toContactRecord(
if (conversation.get('hideStory') !== undefined) { if (conversation.get('hideStory') !== undefined) {
contactRecord.hideStory = Boolean(conversation.get('hideStory')); contactRecord.hideStory = Boolean(conversation.get('hideStory'));
} }
contactRecord.unregisteredAtTimestamp = getSafeLongFromTimestamp(
conversation.get('firstUnregisteredAt')
);
applyUnknownFields(contactRecord, conversation); 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( const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
await toContactRecord(conversation), await toContactRecord(conversation),
contactRecord, contactRecord,

View file

@ -1,9 +1,10 @@
// 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 { 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({ export function isConversationUnregistered({
discoveredUnregisteredAt, discoveredUnregisteredAt,
@ -13,3 +14,11 @@ export function isConversationUnregistered({
isMoreRecentThan(discoveredUnregisteredAt, SIX_HOURS) isMoreRecentThan(discoveredUnregisteredAt, SIX_HOURS)
); );
} }
export function isConversationUnregisteredAndStale({
firstUnregisteredAt,
}: Readonly<{ firstUnregisteredAt?: number }>): boolean {
return Boolean(
firstUnregisteredAt && isOlderThan(firstUnregisteredAt, MONTH)
);
}