From 3b4106d9dd298ff2b5ec58fe2dfe5dd7befcead4 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:53:47 -0800 Subject: [PATCH] Change order of syncs during linking --- ts/background.ts | 150 +++++++++---------- ts/services/storage.ts | 4 +- ts/services/storageRecordOps.ts | 34 ++++- ts/test-mock/storage/message_request_test.ts | 36 ++++- ts/textsecure/MessageReceiver.ts | 8 +- 5 files changed, 142 insertions(+), 90 deletions(-) diff --git a/ts/background.ts b/ts/background.ts index 0fdcef0be5..e9c119b65a 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { webFrame } from 'electron'; -import { isNumber, noop } from 'lodash'; +import { isNumber } from 'lodash'; import { bindActionCreators } from 'redux'; import { render } from 'react-dom'; import { batch as batchDispatch } from 'react-redux'; @@ -13,7 +13,7 @@ import type { ProcessedDataMessage, } from './textsecure/Types.d'; import { HTTPError } from './textsecure/Errors'; -import { +import createTaskWithTimeout, { suspendTasksWithTimeout, resumeTasksWithTimeout, } from './textsecure/TaskWithTimeout'; @@ -34,7 +34,6 @@ import * as durations from './util/durations'; import { explodePromise } from './util/explodePromise'; import { isWindowDragElement } from './util/isWindowDragElement'; import { assert, strictAssert } from './util/assert'; -import { dropNull } from './util/dropNull'; import { normalizeUuid } from './util/normalizeUuid'; import { filter } from './util/iterables'; import { isNotNil } from './util/isNotNil'; @@ -80,11 +79,11 @@ import type { SentEventData, StickerPackEvent, TypingEvent, + VerifiedEvent, ViewEvent, ViewOnceOpenSyncEvent, ViewSyncEvent, } from './textsecure/messageReceiverEvents'; -import { VerifiedEvent } from './textsecure/messageReceiverEvents'; import type { WebAPIType } from './textsecure/WebAPI'; import * as KeyChangeListener from './textsecure/KeyChangeListener'; import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener'; @@ -1709,12 +1708,6 @@ export async function startApp(): Promise { window.reduxActions.app.openInstaller(); } - window.Whisper.events.on('contactsync', () => { - if (window.reduxStore.getState().app.appView === AppViewType.Installer) { - window.reduxActions.app.openInbox(); - } - }); - window.registerForActive(() => notificationService.clear()); window.addEventListener('unload', () => notificationService.fastClear()); @@ -2082,6 +2075,7 @@ export async function startApp(): Promise { } if (firstRun === true && deviceId !== 1) { + const { messaging } = window.textsecure; const hasThemeSetting = Boolean(window.storage.get('theme-setting')); if ( !hasThemeSetting && @@ -2093,19 +2087,71 @@ export async function startApp(): Promise { ); themeChanged(); } - const syncRequest = window.getSyncRequest(); - window.Whisper.events.trigger('contactsync:begin'); - syncRequest.addEventListener('success', () => { - log.info('sync successful'); - window.storage.put('synced_at', Date.now()); - window.Whisper.events.trigger('contactsync'); - runStorageService(); - }); - syncRequest.addEventListener('timeout', () => { - log.error('sync timed out'); - window.Whisper.events.trigger('contactsync'); - runStorageService(); - }); + + const waitForEvent = createTaskWithTimeout( + (event: string): Promise => { + const { promise, resolve } = explodePromise(); + window.Whisper.events.once(event, () => resolve()); + return promise; + }, + 'firstRun:waitForEvent' + ); + + let storageServiceSyncComplete: Promise; + if (window.ConversationController.areWePrimaryDevice()) { + storageServiceSyncComplete = Promise.resolve(); + } else { + storageServiceSyncComplete = waitForEvent( + 'storageService:syncComplete' + ); + } + + const contactSyncComplete = waitForEvent('contactSync:complete'); + + log.info('firstRun: requesting initial sync'); + + // Request configuration, block, GV1 sync messages, contacts + // (only avatars and inboxPosition),and Storage Service sync. + try { + await Promise.all([ + singleProtoJobQueue.add( + messaging.getRequestConfigurationSyncMessage() + ), + singleProtoJobQueue.add(messaging.getRequestBlockSyncMessage()), + singleProtoJobQueue.add(messaging.getRequestGroupSyncMessage()), + singleProtoJobQueue.add(messaging.getRequestContactSyncMessage()), + runStorageService(), + ]); + } catch (error) { + log.error( + 'connect: Failed to request initial syncs', + Errors.toLogFormat(error) + ); + } + + log.info('firstRun: waiting for storage service and contact sync'); + + try { + await Promise.all([storageServiceSyncComplete, contactSyncComplete]); + } catch (error) { + log.error( + 'connect: Failed to run storage service and contact syncs', + Errors.toLogFormat(error) + ); + } + + log.info('firstRun: disabling post link experience'); + window.Signal.Util.postLinkExperience.stop(); + + // Switch to inbox view even if contact sync is still running + if ( + window.reduxStore.getState().app.appView === AppViewType.Installer + ) { + log.info('firstRun: opening inbox'); + window.reduxActions.app.openInbox(); + } else { + log.info('firstRun: not opening inbox'); + } const installedStickerPacks = Stickers.getInstalledStickerPacks(); if (installedStickerPacks.length) { @@ -2122,9 +2168,10 @@ export async function startApp(): Promise { return; } + log.info('firstRun: requesting stickers', operations.length); try { await singleProtoJobQueue.add( - window.textsecure.messaging.getStickerPackSync(operations) + messaging.getStickerPackSync(operations) ); } catch (error) { log.error( @@ -2134,16 +2181,7 @@ export async function startApp(): Promise { } } - try { - await singleProtoJobQueue.add( - window.textsecure.messaging.getRequestKeySyncMessage() - ); - } catch (error) { - log.error( - 'Failed to queue request key sync message', - Errors.toLogFormat(error) - ); - } + log.info('firstRun: done'); } window.storage.onready(async () => { @@ -2486,24 +2524,12 @@ export async function startApp(): Promise { async function onContactSyncComplete() { log.info('onContactSyncComplete'); await window.storage.put('synced_at', Date.now()); + window.Whisper.events.trigger('contactSync:complete'); } async function onContactReceived(ev: ContactEvent) { const details = ev.contactDetails; - if ( - (details.number && - details.number === window.textsecure.storage.user.getNumber()) || - (details.uuid && - details.uuid === window.textsecure.storage.user.getUuid()?.toString()) - ) { - // special case for syncing details about ourselves - if (details.profileKey) { - log.info('Got sync message with our own profile key'); - ourProfileKeyService.set(details.profileKey); - } - } - const c = new window.Whisper.Conversation({ e164: details.number, uuid: details.uuid, @@ -2528,19 +2554,6 @@ export async function startApp(): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const conversation = window.ConversationController.get(detailsId)!; - if (details.profileKey && details.profileKey.length > 0) { - const profileKey = Bytes.toBase64(details.profileKey); - conversation.setProfileKey(profileKey); - } - - if (typeof details.blocked !== 'undefined') { - if (details.blocked) { - conversation.block(); - } else { - conversation.unblock(); - } - } - conversation.set({ name: details.name, inbox_position: details.inboxPosition, @@ -2569,6 +2582,8 @@ export async function startApp(): Promise { window.Signal.Data.updateConversation(conversation.attributes); + // expireTimer isn't stored in Storage Service so we have to rely on the + // contact sync. const { expireTimer } = details; const isValidExpireTimer = typeof expireTimer === 'number'; if (isValidExpireTimer) { @@ -2585,21 +2600,6 @@ export async function startApp(): Promise { ); } - if (details.verified) { - const { verified } = details; - const verifiedEvent = new VerifiedEvent( - { - state: dropNull(verified.state), - destination: dropNull(verified.destination), - destinationUuid: dropNull(verified.destinationUuid), - identityKey: dropNull(verified.identityKey), - viaContactSync: true, - }, - noop - ); - await onVerified(verifiedEvent); - } - if (window.Signal.Util.postLinkExperience.isActive()) { log.info( 'onContactReceived: Adding the message history disclaimer on link' diff --git a/ts/services/storage.ts b/ts/services/storage.ts index aa5b1a60af..74b48e32eb 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -1312,7 +1312,6 @@ async function sync( ); } - window.Signal.Util.postLinkExperience.stop(); log.info('storageService.sync: complete'); return manifest; } @@ -1453,6 +1452,9 @@ export const runStorageServiceSyncJob = debounce(() => { ourProfileKeyService.blockGetWithPromise( storageJobQueue(async () => { await sync(); + + // Notify listeners about sync completion + window.Whisper.events.trigger('storageService:syncComplete'); }, `sync v${window.storage.get('manifestVersion')}`) ); }, 500); diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 23b6ce8c75..cd53f3ae30 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -13,6 +13,7 @@ import { waitThenRespondToGroupV2Migration, } from '../groups'; import { assert } from '../util/assert'; +import { dropNull } from '../util/dropNull'; import { normalizeUuid } from '../util/normalizeUuid'; import { missingCaseError } from '../util/missingCaseError'; import { @@ -768,8 +769,8 @@ export async function mergeContactRecord( : undefined, }; - const e164 = contactRecord.serviceE164 || undefined; - const uuid = contactRecord.serviceUuid || undefined; + const e164 = dropNull(contactRecord.serviceE164); + const uuid = dropNull(contactRecord.serviceUuid); // All contacts must have UUID if (!uuid) { @@ -802,6 +803,25 @@ export async function mergeContactRecord( }); } + const remoteName = dropNull(contactRecord.givenName); + const remoteFamilyName = dropNull(contactRecord.familyName); + const localName = conversation.get('profileName'); + const localFamilyName = conversation.get('profileFamilyName'); + if ( + remoteName && + (localName !== remoteName || localFamilyName !== remoteFamilyName) + ) { + // Local name doesn't match remote name, fetch profile + if (localName) { + conversation.getProfiles(); + } else { + conversation.set({ + profileName: remoteName, + profileFamilyName: remoteFamilyName, + }); + } + } + const verified = await conversation.safeGetVerified(); const storageServiceVerified = contactRecord.identityState || 0; if (verified !== storageServiceVerified) { @@ -1065,6 +1085,16 @@ export async function mergeAccountRecord( remotelyPinnedConversations.forEach(conversation => { conversation.set({ isPinned: true, isArchived: false }); + + if ( + window.Signal.Util.postLinkExperience.isActive() && + isGroupV2(conversation.attributes) + ) { + log.info( + 'mergeAccountRecord: Adding the message history disclaimer on link' + ); + conversation.addMessageHistoryDisclaimer(); + } updateConversation(conversation.attributes); }); diff --git a/ts/test-mock/storage/message_request_test.ts b/ts/test-mock/storage/message_request_test.ts index 09373cd544..78ff48d4d4 100644 --- a/ts/test-mock/storage/message_request_test.ts +++ b/ts/test-mock/storage/message_request_test.ts @@ -25,6 +25,8 @@ describe('storage service', function needsName() { it('should handle message request state changes', async () => { const { phone, desktop, server } = bootstrap; + const initialState = await phone.expectStorageState('initial state'); + debug('Creating stranger'); const stranger = await server.createPrimaryDevice({ profileName: 'Mysterious Stranger', @@ -52,9 +54,23 @@ describe('storage service', function needsName() { ) .click(); - const initialState = await phone.expectStorageState('initial state'); - assert.strictEqual(initialState.version, 1); - assert.isUndefined(initialState.getContact(stranger)); + debug("Verify that we stored stranger's profile key"); + const postMessageState = await phone.waitForStorageState({ + after: initialState, + }); + { + assert.strictEqual(postMessageState.version, 2); + assert.isFalse(postMessageState.getContact(stranger)?.whitelisted); + assert.strictEqual( + postMessageState.getContact(stranger)?.profileKey?.length, + 32 + ); + + // ContactRecord + const { added, removed } = postMessageState.diff(initialState); + assert.strictEqual(added.length, 1, 'only one record must be added'); + assert.strictEqual(removed.length, 0, 'no records should be removed'); + } debug('Accept conversation from a stranger'); await conversationStack @@ -64,15 +80,19 @@ describe('storage service', function needsName() { debug('Verify that storage state was updated'); { const nextState = await phone.waitForStorageState({ - after: initialState, + after: postMessageState, }); - assert.strictEqual(nextState.version, 2); + assert.strictEqual(nextState.version, 3); assert.isTrue(nextState.getContact(stranger)?.whitelisted); // ContactRecord - const { added, removed } = nextState.diff(initialState); + const { added, removed } = nextState.diff(postMessageState); assert.strictEqual(added.length, 1, 'only one record must be added'); - assert.strictEqual(removed.length, 0, 'no records should be removed'); + assert.strictEqual( + removed.length, + 1, + 'only one record should be removed' + ); } // Stranger should receive our profile key @@ -110,6 +130,6 @@ describe('storage service', function needsName() { debug('Verifying the final manifest version'); const finalState = await phone.expectStorageState('consistency check'); - assert.strictEqual(finalState.version, 2); + assert.strictEqual(finalState.version, 3); }); }); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 5b9e4459ee..4c109dc227 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -2610,7 +2610,7 @@ export default class MessageReceiver envelope: ProcessedEnvelope, contacts: Proto.SyncMessage.IContacts ): Promise { - log.info('contact sync'); + log.info('MessageReceiver: handleContacts'); const { blob } = contacts; if (!blob) { throw new Error('MessageReceiver.handleContacts: blob field was missing'); @@ -2631,11 +2631,11 @@ export default class MessageReceiver contactDetails = contactBuffer.next(); } - const finalEvent = new ContactSyncEvent(); - results.push(this.dispatchAndWait(finalEvent)); - await Promise.all(results); + const finalEvent = new ContactSyncEvent(); + await this.dispatchAndWait(finalEvent); + log.info('handleContacts: finished'); }