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 {
|
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
1
ts/model-types.d.ts
vendored
|
@ -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>;
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue