Support unregisteredAtTimestamp in storage service
This commit is contained in:
parent
6936cc1e2e
commit
62647a357f
6 changed files with 154 additions and 34 deletions
|
@ -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
1
ts/model-types.d.ts
vendored
|
@ -270,6 +270,7 @@ export type ConversationAttributesType = {
|
|||
customColor?: CustomColorType;
|
||||
customColorId?: string;
|
||||
discoveredUnregisteredAt?: number;
|
||||
firstUnregisteredAt?: number;
|
||||
draftChanged?: boolean;
|
||||
draftAttachments?: Array<AttachmentDraftType>;
|
||||
draftBodyRanges?: Array<BodyRangeType>;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue