Refactor app initialization logic
This commit is contained in:
parent
205c477082
commit
4c3db76bde
17 changed files with 394 additions and 582 deletions
|
@ -37,6 +37,7 @@ global.window = {
|
||||||
storage: {
|
storage: {
|
||||||
get: key => storageMap.get(key),
|
get: key => storageMap.get(key),
|
||||||
put: async (key, value) => storageMap.set(key, value),
|
put: async (key, value) => storageMap.set(key, value),
|
||||||
|
remove: async key => storageMap.clear(key),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { uuidToBytes } from './util/uuidToBytes';
|
||||||
import { dropNull } from './util/dropNull';
|
import { dropNull } from './util/dropNull';
|
||||||
import { HashType } from './types/Crypto';
|
import { HashType } from './types/Crypto';
|
||||||
import { getCountryCode } from './types/PhoneNumber';
|
import { getCountryCode } from './types/PhoneNumber';
|
||||||
|
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
||||||
|
|
||||||
export type ConfigKeyType =
|
export type ConfigKeyType =
|
||||||
| 'desktop.calling.ringrtcAdmFull'
|
| 'desktop.calling.ringrtcAdmFull'
|
||||||
|
@ -135,13 +136,23 @@ export const _refreshRemoteConfig = async (
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// If remote configuration fetch worked - we are not expired anymore.
|
const remoteExpirationValue = getValue('desktop.clientExpiration');
|
||||||
if (
|
if (!remoteExpirationValue) {
|
||||||
!getValue('desktop.clientExpiration') &&
|
// If remote configuration fetch worked - we are not expired anymore.
|
||||||
window.storage.get('remoteBuildExpiration') != null
|
if (window.storage.get('remoteBuildExpiration') != null) {
|
||||||
) {
|
log.warn('Remote Config: clearing remote expiration on successful fetch');
|
||||||
log.warn('Remote Config: clearing remote expiration on successful fetch');
|
}
|
||||||
await window.storage.remove('remoteBuildExpiration');
|
await window.storage.remove('remoteBuildExpiration');
|
||||||
|
} else {
|
||||||
|
const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(
|
||||||
|
remoteExpirationValue
|
||||||
|
);
|
||||||
|
if (remoteBuildExpirationTimestamp) {
|
||||||
|
await window.storage.put(
|
||||||
|
'remoteBuildExpiration',
|
||||||
|
remoteBuildExpirationTimestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.storage.put('remoteConfig', config);
|
await window.storage.put('remoteConfig', config);
|
||||||
|
|
683
ts/background.ts
683
ts/background.ts
|
@ -69,7 +69,7 @@ import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
||||||
import { ourProfileKeyService } from './services/ourProfileKey';
|
import { ourProfileKeyService } from './services/ourProfileKey';
|
||||||
import { notificationService } from './services/notifications';
|
import { notificationService } from './services/notifications';
|
||||||
import { areWeASubscriberService } from './services/areWeASubscriber';
|
import { areWeASubscriberService } from './services/areWeASubscriber';
|
||||||
import { onContactSync, setIsInitialSync } from './services/contactSync';
|
import { onContactSync, setIsInitialContactSync } from './services/contactSync';
|
||||||
import { startTimeTravelDetector } from './util/startTimeTravelDetector';
|
import { startTimeTravelDetector } from './util/startTimeTravelDetector';
|
||||||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||||
import { LatestQueue } from './util/LatestQueue';
|
import { LatestQueue } from './util/LatestQueue';
|
||||||
|
@ -206,6 +206,8 @@ import {
|
||||||
import { postSaveUpdates } from './util/cleanup';
|
import { postSaveUpdates } from './util/cleanup';
|
||||||
import { handleDataMessage } from './messages/handleDataMessage';
|
import { handleDataMessage } from './messages/handleDataMessage';
|
||||||
import { MessageModel } from './models/messages';
|
import { MessageModel } from './models/messages';
|
||||||
|
import { waitForEvent } from './shims/events';
|
||||||
|
import { sendSyncRequests } from './textsecure/syncRequests';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
|
@ -402,7 +404,6 @@ export async function startApp(): Promise<void> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let accountManager: AccountManager;
|
let accountManager: AccountManager;
|
||||||
let isInRegistration = false;
|
|
||||||
window.getAccountManager = () => {
|
window.getAccountManager = () => {
|
||||||
if (accountManager) {
|
if (accountManager) {
|
||||||
return accountManager;
|
return accountManager;
|
||||||
|
@ -413,19 +414,23 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
accountManager = new window.textsecure.AccountManager(server);
|
accountManager = new window.textsecure.AccountManager(server);
|
||||||
accountManager.addEventListener('startRegistration', () => {
|
accountManager.addEventListener('startRegistration', () => {
|
||||||
isInRegistration = true;
|
|
||||||
pauseProcessing();
|
pauseProcessing();
|
||||||
|
// We should already be logged out, but this ensures that the next time we connect
|
||||||
|
// to the auth socket it is from newly-registered credentials
|
||||||
|
drop(server?.logout());
|
||||||
|
authSocketConnectCount = 0;
|
||||||
|
|
||||||
backupReady.reject(new Error('startRegistration'));
|
backupReady.reject(new Error('startRegistration'));
|
||||||
backupReady = explodePromise();
|
backupReady = explodePromise();
|
||||||
|
registrationCompleted = explodePromise();
|
||||||
});
|
});
|
||||||
accountManager.addEventListener('registration', () => {
|
|
||||||
isInRegistration = false;
|
accountManager.addEventListener('endRegistration', () => {
|
||||||
window.Whisper.events.trigger('userChanged', false);
|
window.Whisper.events.trigger('userChanged', false);
|
||||||
|
|
||||||
|
drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete'));
|
||||||
|
registrationCompleted?.resolve();
|
||||||
drop(Registration.markDone());
|
drop(Registration.markDone());
|
||||||
log.info('dispatching registration event');
|
|
||||||
window.Whisper.events.trigger('registration_done');
|
|
||||||
});
|
});
|
||||||
return accountManager;
|
return accountManager;
|
||||||
};
|
};
|
||||||
|
@ -1489,18 +1494,6 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
await runAllSyncTasks();
|
await runAllSyncTasks();
|
||||||
|
|
||||||
log.info('listening for registration events');
|
|
||||||
window.Whisper.events.on('registration_done', () => {
|
|
||||||
log.info('handling registration event');
|
|
||||||
|
|
||||||
strictAssert(server !== undefined, 'WebAPI not ready');
|
|
||||||
|
|
||||||
drop(connect(true));
|
|
||||||
|
|
||||||
// Connect messageReceiver back to websocket
|
|
||||||
drop(afterStart());
|
|
||||||
});
|
|
||||||
|
|
||||||
cancelInitializationMessage();
|
cancelInitializationMessage();
|
||||||
render(
|
render(
|
||||||
window.Signal.State.Roots.createApp(window.reduxStore),
|
window.Signal.State.Roots.createApp(window.reduxStore),
|
||||||
|
@ -1527,7 +1520,7 @@ export async function startApp(): Promise<void> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isCoreDataValid && Registration.everDone()) {
|
if (isCoreDataValid && Registration.everDone()) {
|
||||||
drop(connect());
|
idleDetector.start();
|
||||||
if (window.storage.get('backupDownloadPath')) {
|
if (window.storage.get('backupDownloadPath')) {
|
||||||
window.reduxActions.installer.showBackupImport();
|
window.reduxActions.installer.showBackupImport();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1583,25 +1576,17 @@ export async function startApp(): Promise<void> {
|
||||||
resolveOnAppView = undefined;
|
resolveOnAppView = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(afterStart());
|
setupNetworkChangeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function afterStart() {
|
function setupNetworkChangeListeners() {
|
||||||
strictAssert(messageReceiver, 'messageReceiver must be initialized');
|
|
||||||
strictAssert(server, 'server must be initialized');
|
strictAssert(server, 'server must be initialized');
|
||||||
|
|
||||||
log.info('afterStart(): emitting app-ready-for-processing');
|
|
||||||
window.Whisper.events.trigger('app-ready-for-processing');
|
|
||||||
|
|
||||||
const onOnline = () => {
|
const onOnline = () => {
|
||||||
log.info('background: online');
|
log.info('background: online');
|
||||||
|
drop(afterAuthSocketConnect());
|
||||||
// Do not attempt to connect while expired or in-the-middle of
|
|
||||||
// registration
|
|
||||||
if (!remotelyExpired && !isInRegistration) {
|
|
||||||
drop(connect());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.Whisper.events.on('online', onOnline);
|
window.Whisper.events.on('online', onOnline);
|
||||||
|
|
||||||
const onOffline = () => {
|
const onOffline = () => {
|
||||||
|
@ -1611,7 +1596,7 @@ export async function startApp(): Promise<void> {
|
||||||
const hasAppEverBeenRegistered = Registration.everDone();
|
const hasAppEverBeenRegistered = Registration.everDone();
|
||||||
|
|
||||||
log.info('background: offline', {
|
log.info('background: offline', {
|
||||||
connectCount,
|
authSocketConnectCount,
|
||||||
hasInitialLoadCompleted,
|
hasInitialLoadCompleted,
|
||||||
appView,
|
appView,
|
||||||
hasAppEverBeenRegistered,
|
hasAppEverBeenRegistered,
|
||||||
|
@ -1620,7 +1605,11 @@ export async function startApp(): Promise<void> {
|
||||||
drop(challengeHandler?.onOffline());
|
drop(challengeHandler?.onOffline());
|
||||||
drop(AttachmentDownloadManager.stop());
|
drop(AttachmentDownloadManager.stop());
|
||||||
drop(AttachmentBackupManager.stop());
|
drop(AttachmentBackupManager.stop());
|
||||||
drop(messageReceiver?.drain());
|
|
||||||
|
if (messageReceiver) {
|
||||||
|
drop(messageReceiver.drain());
|
||||||
|
server?.unregisterRequestHandler(messageReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
if (hasAppEverBeenRegistered) {
|
if (hasAppEverBeenRegistered) {
|
||||||
const state = window.reduxStore.getState();
|
const state = window.reduxStore.getState();
|
||||||
|
@ -1653,14 +1642,171 @@ export async function startApp(): Promise<void> {
|
||||||
} else if (server.isOnline() === false) {
|
} else if (server.isOnline() === false) {
|
||||||
onOffline();
|
onOffline();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let backupReady = explodePromise<{ wasBackupImported: boolean }>();
|
||||||
|
let registrationCompleted: ExplodePromiseResultType<void> | undefined;
|
||||||
|
let authSocketConnectCount = 0;
|
||||||
|
let afterAuthSocketConnectPromise: ExplodePromiseResultType<void> | undefined;
|
||||||
|
let remotelyExpired = false;
|
||||||
|
|
||||||
|
async function afterAuthSocketConnect() {
|
||||||
|
let contactSyncComplete: Promise<void> | undefined;
|
||||||
|
let storageServiceSyncComplete: Promise<void> | undefined;
|
||||||
|
let hasSentSyncRequests = false;
|
||||||
|
|
||||||
|
const isFirstAuthSocketConnect = authSocketConnectCount === 0;
|
||||||
|
const logId = `afterAuthSocketConnect.${authSocketConnectCount}`;
|
||||||
|
|
||||||
|
authSocketConnectCount += 1;
|
||||||
|
|
||||||
|
if (remotelyExpired) {
|
||||||
|
log.info('afterAuthSocketConnect: remotely expired');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strictAssert(server, 'server must be initialized');
|
||||||
|
strictAssert(messageReceiver, 'messageReceiver must be initialized');
|
||||||
|
|
||||||
|
while (afterAuthSocketConnectPromise?.promise) {
|
||||||
|
log.info(`${logId}: waiting for previous run to finish`);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await afterAuthSocketConnectPromise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAuthSocketConnectPromise = explodePromise();
|
||||||
|
log.info(`${logId}: starting`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Await any ongoing registration
|
||||||
|
if (registrationCompleted) {
|
||||||
|
log.info(`${logId}: awaiting completion of registration`);
|
||||||
|
await registrationCompleted?.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.textsecure.storage.user.getAci()) {
|
||||||
|
log.error(`${logId}: ACI not captured during registration, unlinking`);
|
||||||
|
return unlinkAndDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.textsecure.storage.user.getPni()) {
|
||||||
|
log.error(`${logId}: PNI not captured during registration, unlinking`);
|
||||||
|
return unlinkAndDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch remote config, before we process the message queue
|
||||||
|
if (isFirstAuthSocketConnect) {
|
||||||
|
try {
|
||||||
|
await window.Signal.RemoteConfig.forceRefreshRemoteConfig(
|
||||||
|
server,
|
||||||
|
'afterAuthSocketConnect/firstConnect'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`${logId}: Error refreshing remote config:`,
|
||||||
|
isNumber(error.code)
|
||||||
|
? `code: ${error.code}`
|
||||||
|
: Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const postRegistrationSyncsComplete =
|
||||||
|
window.storage.get('postRegistrationSyncsStatus') !== 'incomplete';
|
||||||
|
|
||||||
|
// 3. Send any critical sync requests after registration
|
||||||
|
if (!postRegistrationSyncsComplete) {
|
||||||
|
log.info(`${logId}: postRegistrationSyncs not complete, sending sync`);
|
||||||
|
|
||||||
|
setIsInitialContactSync(true);
|
||||||
|
const syncRequest = await sendSyncRequests();
|
||||||
|
hasSentSyncRequests = true;
|
||||||
|
contactSyncComplete = syncRequest.contactSyncComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Download (or resume download) of link & sync backup
|
||||||
|
const { wasBackupImported } = await maybeDownloadAndImportBackup();
|
||||||
|
log.info(logId, {
|
||||||
|
wasBackupImported,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Kickoff storage service sync
|
||||||
|
if (isFirstAuthSocketConnect || !postRegistrationSyncsComplete) {
|
||||||
|
log.info(`${logId}: triggering storage service sync`);
|
||||||
|
|
||||||
|
storageServiceSyncComplete = waitForEvent(
|
||||||
|
'storageService:syncComplete'
|
||||||
|
);
|
||||||
|
drop(runStorageService({ reason: 'afterFirstAuthSocketConnect' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Start processing messages from websocket
|
||||||
|
log.info(`${logId}: enabling message processing`);
|
||||||
|
server.registerRequestHandler(messageReceiver);
|
||||||
|
messageReceiver.startProcessingQueue();
|
||||||
|
|
||||||
|
// 7. Wait for critical post-registration syncs before showing inbox
|
||||||
|
if (!postRegistrationSyncsComplete) {
|
||||||
|
const syncsToAwaitBeforeShowingInbox = [contactSyncComplete];
|
||||||
|
|
||||||
|
// If backup was imported, we do not need to await the storage service sync
|
||||||
|
if (!wasBackupImported) {
|
||||||
|
syncsToAwaitBeforeShowingInbox.push(storageServiceSyncComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(syncsToAwaitBeforeShowingInbox);
|
||||||
|
await window.storage.put('postRegistrationSyncsStatus', 'complete');
|
||||||
|
log.info(`${logId}: postRegistrationSyncs complete`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`${logId}: Failed to run postRegistrationSyncs`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Show inbox
|
||||||
|
const state = window.reduxStore.getState();
|
||||||
|
if (state.app.appView === AppViewType.Installer) {
|
||||||
|
log.info(`${logId}: switching from installer to inbox`);
|
||||||
|
window.reduxActions.app.openInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Start services requiring auth connection
|
||||||
|
afterEveryAuthConnect();
|
||||||
|
|
||||||
|
// 10. Handle once-on-boot tasks
|
||||||
|
if (isFirstAuthSocketConnect) {
|
||||||
|
afterEveryLinkedStartup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Handle infrequent once-on-new-version tasks
|
||||||
|
if (newVersion) {
|
||||||
|
drop(
|
||||||
|
afterEveryLinkedStartupOnNewVersion({
|
||||||
|
skipSyncRequests: hasSentSyncRequests,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`${logId}: error`, Errors.toLogFormat(e));
|
||||||
|
} finally {
|
||||||
|
afterAuthSocketConnectPromise?.resolve();
|
||||||
|
afterAuthSocketConnectPromise = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeDownloadAndImportBackup(): Promise<{
|
||||||
|
wasBackupImported: boolean;
|
||||||
|
}> {
|
||||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||||
if (backupDownloadPath) {
|
if (backupDownloadPath) {
|
||||||
tapToViewMessagesDeletionService.pause();
|
tapToViewMessagesDeletionService.pause();
|
||||||
|
|
||||||
// Download backup before enabling request handler and storage service
|
// Download backup before enabling request handler and storage service
|
||||||
try {
|
try {
|
||||||
await backupsService.downloadAndImport({
|
const { wasBackupImported } = await backupsService.downloadAndImport({
|
||||||
onProgress: (backupStep, currentBytes, totalBytes) => {
|
onProgress: (backupStep, currentBytes, totalBytes) => {
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
backupStep,
|
backupStep,
|
||||||
|
@ -1670,36 +1816,121 @@ export async function startApp(): Promise<void> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('afterStart: backup download attempt completed, resolving');
|
log.info('afterAppStart: backup download attempt completed, resolving');
|
||||||
backupReady.resolve();
|
backupReady.resolve({ wasBackupImported });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('afterStart: backup download failed, rejecting');
|
log.error('afterAppStart: backup download failed, rejecting');
|
||||||
backupReady.reject(error);
|
backupReady.reject(error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
tapToViewMessagesDeletionService.resume();
|
tapToViewMessagesDeletionService.resume();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
backupReady.resolve();
|
backupReady.resolve({ wasBackupImported: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
server.registerRequestHandler(messageReceiver);
|
return backupReady.promise;
|
||||||
drop(runStorageService({ reason: 'afterStart' }));
|
|
||||||
|
|
||||||
// Opposite of `messageReceiver.stopProcessing`
|
|
||||||
messageReceiver.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.getSyncRequest = (timeoutMillis?: number) => {
|
function afterEveryLinkedStartup() {
|
||||||
strictAssert(messageReceiver, 'MessageReceiver not initialized');
|
log.info('afterAuthSocketConnect/afterEveryLinkedStartup');
|
||||||
|
|
||||||
const syncRequest = new window.textsecure.SyncRequest(
|
// Note: we always have to register our capabilities all at once, so we do this
|
||||||
messageReceiver,
|
// after connect on every startup
|
||||||
timeoutMillis
|
drop(registerCapabilities());
|
||||||
);
|
drop(ensureAEP());
|
||||||
syncRequest.start();
|
drop(maybeQueueDeviceNameFetch());
|
||||||
return syncRequest;
|
Stickers.downloadQueuedPacks();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
async function afterEveryLinkedStartupOnNewVersion({
|
||||||
|
skipSyncRequests = false,
|
||||||
|
}: {
|
||||||
|
skipSyncRequests: boolean;
|
||||||
|
}) {
|
||||||
|
log.info('afterAuthSocketConnect/afterEveryLinkedStartupOnNewVersion');
|
||||||
|
|
||||||
|
if (window.ConversationController.areWePrimaryDevice()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!skipSyncRequests) {
|
||||||
|
drop(sendSyncRequests());
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(StorageService.reprocessUnknownFields());
|
||||||
|
|
||||||
|
const manager = window.getAccountManager();
|
||||||
|
await Promise.all([
|
||||||
|
manager.maybeUpdateDeviceName(),
|
||||||
|
window.textsecure.storage.user.removeSignalingKey(),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(
|
||||||
|
"Problem with 'afterLinkedStartupOnNewVersion' tasks: ",
|
||||||
|
Errors.toLogFormat(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAEP() {
|
||||||
|
if (
|
||||||
|
window.storage.get('accountEntropyPool') ||
|
||||||
|
window.ConversationController.areWePrimaryDevice()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSent =
|
||||||
|
window.storage.get('accountEntropyPoolLastRequestTime') ?? 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// If we last attempted sync one day in the past, or if we time
|
||||||
|
// traveled.
|
||||||
|
if (isOlderThan(lastSent, DAY) || lastSent > now) {
|
||||||
|
log.warn('ensureAEP: AEP not captured, requesting sync');
|
||||||
|
await singleProtoJobQueue.add(MessageSender.getRequestKeySyncMessage());
|
||||||
|
await window.storage.put('accountEntropyPoolLastRequestTime', now);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
'ensureAEP: AEP not captured, but sync requested recently.' +
|
||||||
|
'Not running'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerCapabilities() {
|
||||||
|
strictAssert(server, 'server must be initialized');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.registerCapabilities({
|
||||||
|
deleteSync: true,
|
||||||
|
versionedExpirationTimer: true,
|
||||||
|
ssre2: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'Error: Unable to register our capabilities.',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterEveryAuthConnect() {
|
||||||
|
log.info('afterAuthSocketConnect/afterEveryAuthConnect');
|
||||||
|
|
||||||
|
strictAssert(challengeHandler, 'afterEveryAuthConnect: challengeHandler');
|
||||||
|
drop(challengeHandler.onOnline());
|
||||||
|
reconnectBackOff.reset();
|
||||||
|
drop(window.Signal.Services.initializeGroupCredentialFetcher());
|
||||||
|
drop(AttachmentDownloadManager.start());
|
||||||
|
|
||||||
|
if (isBackupEnabled()) {
|
||||||
|
backupsService.start();
|
||||||
|
drop(AttachmentBackupManager.start());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onNavigatorOffline() {
|
function onNavigatorOffline() {
|
||||||
log.info('background: navigator offline');
|
log.info('background: navigator offline');
|
||||||
|
@ -1752,351 +1983,6 @@ export async function startApp(): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupReady = explodePromise<void>();
|
|
||||||
|
|
||||||
let connectCount = 0;
|
|
||||||
let connectPromise: ExplodePromiseResultType<void> | undefined;
|
|
||||||
let remotelyExpired = false;
|
|
||||||
async function connect(firstRun?: boolean) {
|
|
||||||
if (connectPromise && !firstRun) {
|
|
||||||
log.warn('background: connect already running', {
|
|
||||||
connectCount,
|
|
||||||
firstRun,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (connectPromise && firstRun) {
|
|
||||||
while (connectPromise) {
|
|
||||||
log.warn(
|
|
||||||
'background: connect already running; waiting for previous run',
|
|
||||||
{
|
|
||||||
connectCount,
|
|
||||||
firstRun,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await connectPromise.promise;
|
|
||||||
}
|
|
||||||
await connect(firstRun);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remotelyExpired) {
|
|
||||||
log.warn('background: remotely expired, not reconnecting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
strictAssert(server !== undefined, 'WebAPI not connected');
|
|
||||||
|
|
||||||
let contactSyncComplete: Promise<void> | undefined;
|
|
||||||
const areWePrimaryDevice =
|
|
||||||
window.ConversationController.areWePrimaryDevice();
|
|
||||||
|
|
||||||
const waitForEvent = createTaskWithTimeout(
|
|
||||||
(event: string): Promise<void> => {
|
|
||||||
const { promise, resolve } = explodePromise<void>();
|
|
||||||
window.Whisper.events.once(event, () => resolve());
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
'connect:waitForEvent',
|
|
||||||
{ timeout: 2 * durations.MINUTE }
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
connectPromise = explodePromise();
|
|
||||||
|
|
||||||
if (firstRun === true && !areWePrimaryDevice) {
|
|
||||||
contactSyncComplete = waitForEvent('contactSync:complete');
|
|
||||||
|
|
||||||
// Send the contact sync message immediately (don't wait until after backup is
|
|
||||||
// downloaded & imported)
|
|
||||||
await singleProtoJobQueue.add(
|
|
||||||
MessageSender.getRequestContactSyncMessage()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for backup to be downloaded
|
|
||||||
try {
|
|
||||||
await backupReady.promise;
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'background: backup download failed, not reconnecting',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info('background: connect unblocked by backups');
|
|
||||||
|
|
||||||
// Reset the flag and update it below if needed
|
|
||||||
setIsInitialSync(false);
|
|
||||||
|
|
||||||
if (!Registration.everDone()) {
|
|
||||||
log.info('background: registration not done, not connecting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('background: connect', { firstRun, connectCount });
|
|
||||||
|
|
||||||
// Update our profile key in the conversation if we just got linked.
|
|
||||||
const profileKey = await ourProfileKeyService.get();
|
|
||||||
if (firstRun && profileKey) {
|
|
||||||
const me = window.ConversationController.getOurConversation();
|
|
||||||
strictAssert(me !== undefined, "Didn't find newly created ourselves");
|
|
||||||
await me.setProfileKey(Bytes.toBase64(profileKey), {
|
|
||||||
reason: 'connect/firstRun',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBackupEnabled()) {
|
|
||||||
backupsService.start();
|
|
||||||
drop(AttachmentBackupManager.start());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectCount === 0 || firstRun) {
|
|
||||||
try {
|
|
||||||
// Force a re-fetch before we process our queue. We may want to turn on
|
|
||||||
// something which changes how we process incoming messages!
|
|
||||||
await window.Signal.RemoteConfig.forceRefreshRemoteConfig(
|
|
||||||
server,
|
|
||||||
`connectCount=${connectCount} firstRun=${firstRun}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const expiration = window.Signal.RemoteConfig.getValue(
|
|
||||||
'desktop.clientExpiration'
|
|
||||||
);
|
|
||||||
if (expiration) {
|
|
||||||
const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(
|
|
||||||
expiration as string
|
|
||||||
);
|
|
||||||
if (remoteBuildExpirationTimestamp) {
|
|
||||||
await window.storage.put(
|
|
||||||
'remoteBuildExpiration',
|
|
||||||
remoteBuildExpirationTimestamp
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'connect: Error refreshing remote config:',
|
|
||||||
isNumber(error.code)
|
|
||||||
? `code: ${error.code}`
|
|
||||||
: Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectCount += 1;
|
|
||||||
|
|
||||||
void window.Signal.Services.initializeGroupCredentialFetcher();
|
|
||||||
|
|
||||||
drop(AttachmentDownloadManager.start());
|
|
||||||
|
|
||||||
if (connectCount === 1) {
|
|
||||||
Stickers.downloadQueuedPacks();
|
|
||||||
if (!newVersion) {
|
|
||||||
drop(runStorageService({ reason: 'connect/connectCount=1' }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On startup after upgrading to a new version, request a contact sync
|
|
||||||
// (but only if we're not the primary device)
|
|
||||||
if (
|
|
||||||
!firstRun &&
|
|
||||||
connectCount === 1 &&
|
|
||||||
newVersion &&
|
|
||||||
!areWePrimaryDevice
|
|
||||||
) {
|
|
||||||
log.info('Boot after upgrading. Requesting contact sync');
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.getSyncRequest();
|
|
||||||
|
|
||||||
void StorageService.reprocessUnknownFields();
|
|
||||||
void runStorageService({ reason: 'connect/bootAfterUpgrade' });
|
|
||||||
|
|
||||||
const manager = window.getAccountManager();
|
|
||||||
await Promise.all([
|
|
||||||
manager.maybeUpdateDeviceName(),
|
|
||||||
window.textsecure.storage.user.removeSignalingKey(),
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(
|
|
||||||
"Problem with 'boot after upgrade' tasks: ",
|
|
||||||
Errors.toLogFormat(e)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!window.textsecure.storage.user.getAci()) {
|
|
||||||
log.error('UUID not captured during registration, unlinking');
|
|
||||||
return unlinkAndDisconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!window.textsecure.storage.user.getPni()) {
|
|
||||||
log.error('PNI not captured during registration, unlinking softly');
|
|
||||||
return unlinkAndDisconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectCount === 1) {
|
|
||||||
try {
|
|
||||||
// Note: we always have to register our capabilities all at once, so we do this
|
|
||||||
// after connect on every startup
|
|
||||||
await server.registerCapabilities({
|
|
||||||
deleteSync: true,
|
|
||||||
versionedExpirationTimer: true,
|
|
||||||
ssre2: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'Error: Unable to register our capabilities.',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have the correct device name locally (allowing us to get eventually
|
|
||||||
// consistent with primary, in case we failed to process a deviceNameChangeSync
|
|
||||||
// for some reason). We do this after calling `maybeUpdateDeviceName` to ensure
|
|
||||||
// that the device name on server is encrypted.
|
|
||||||
drop(maybeQueueDeviceNameFetch());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstRun === true && !areWePrimaryDevice) {
|
|
||||||
if (!window.storage.get('accountEntropyPool')) {
|
|
||||||
const lastSent =
|
|
||||||
window.storage.get('accountEntropyPoolLastRequestTime') ?? 0;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// If we last attempted sync one day in the past, or if we time
|
|
||||||
// traveled.
|
|
||||||
if (isOlderThan(lastSent, DAY) || lastSent > now) {
|
|
||||||
log.warn('connect: AEP not captured, requesting sync');
|
|
||||||
await singleProtoJobQueue.add(
|
|
||||||
MessageSender.getRequestKeySyncMessage()
|
|
||||||
);
|
|
||||||
await window.storage.put('accountEntropyPoolLastRequestTime', now);
|
|
||||||
} else {
|
|
||||||
log.warn(
|
|
||||||
'connect: AEP not captured, but sync requested recently.' +
|
|
||||||
'Not running'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let storageServiceSyncComplete: Promise<void>;
|
|
||||||
if (window.ConversationController.areWePrimaryDevice()) {
|
|
||||||
storageServiceSyncComplete = Promise.resolve();
|
|
||||||
} else {
|
|
||||||
storageServiceSyncComplete = waitForEvent(
|
|
||||||
'storageService:syncComplete'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('firstRun: requesting initial sync');
|
|
||||||
setIsInitialSync(true);
|
|
||||||
|
|
||||||
// Request configuration, block, GV1 sync messages, contacts
|
|
||||||
// (only avatars and inboxPosition),and Storage Service sync.
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
singleProtoJobQueue.add(
|
|
||||||
MessageSender.getRequestConfigurationSyncMessage()
|
|
||||||
),
|
|
||||||
singleProtoJobQueue.add(MessageSender.getRequestBlockSyncMessage()),
|
|
||||||
runStorageService({ reason: 'firstRun/initialSync' }),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'connect: Failed to request initial syncs',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('firstRun: waiting for storage service and contact sync');
|
|
||||||
strictAssert(
|
|
||||||
contactSyncComplete,
|
|
||||||
'contact sync is awaited during first run'
|
|
||||||
);
|
|
||||||
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: initial sync complete');
|
|
||||||
setIsInitialSync(false);
|
|
||||||
|
|
||||||
// Switch to inbox view even if contact sync is still running
|
|
||||||
const state = window.reduxStore.getState();
|
|
||||||
if (state.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) {
|
|
||||||
const operations = installedStickerPacks.map(pack => ({
|
|
||||||
packId: pack.id,
|
|
||||||
packKey: pack.key,
|
|
||||||
installed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!window.ConversationController.areWePrimaryDevice()) {
|
|
||||||
log.info('firstRun: requesting stickers', operations.length);
|
|
||||||
try {
|
|
||||||
await singleProtoJobQueue.add(
|
|
||||||
MessageSender.getStickerPackSync(operations)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'connect: Failed to queue sticker sync message',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('firstRun: done');
|
|
||||||
} else {
|
|
||||||
const state = window.reduxStore.getState();
|
|
||||||
if (
|
|
||||||
state.app.appView === AppViewType.Installer &&
|
|
||||||
state.installer.step === InstallScreenStep.BackupImport
|
|
||||||
) {
|
|
||||||
log.info('notFirstRun: opening inbox after backup import');
|
|
||||||
window.reduxActions.app.openInbox();
|
|
||||||
} else {
|
|
||||||
log.info('notFirstRun: not opening inbox');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.storage.onready(async () => {
|
|
||||||
idleDetector.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!challengeHandler) {
|
|
||||||
throw new Error('Expected challenge handler to be initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(challengeHandler.onOnline());
|
|
||||||
|
|
||||||
reconnectBackOff.reset();
|
|
||||||
} finally {
|
|
||||||
if (connectPromise) {
|
|
||||||
connectPromise.resolve();
|
|
||||||
connectPromise = undefined;
|
|
||||||
} else {
|
|
||||||
log.warn('background connect: in finally, no connectPromise!', {
|
|
||||||
connectCount,
|
|
||||||
firstRun,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.SignalContext.nativeThemeListener.subscribe(themeChanged);
|
window.SignalContext.nativeThemeListener.subscribe(themeChanged);
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
@ -2215,7 +2101,6 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
log.info('manualConnect: calling connect()');
|
log.info('manualConnect: calling connect()');
|
||||||
enqueueReconnectToWebSocket();
|
enqueueReconnectToWebSocket();
|
||||||
drop(connect());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
||||||
|
|
|
@ -188,7 +188,7 @@ import { explodePromise } from '../util/explodePromise';
|
||||||
import { getCallHistorySelector } from '../state/selectors/callHistory';
|
import { getCallHistorySelector } from '../state/selectors/callHistory';
|
||||||
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
||||||
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
||||||
import { getIsInitialSync } from '../services/contactSync';
|
import { getIsInitialContactSync } from '../services/contactSync';
|
||||||
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
|
||||||
import { cleanupMessages } from '../util/cleanup';
|
import { cleanupMessages } from '../util/cleanup';
|
||||||
import { MessageModel } from './messages';
|
import { MessageModel } from './messages';
|
||||||
|
@ -2348,7 +2348,7 @@ export class ConversationModel extends window.Backbone
|
||||||
await window.MessageCache.saveMessage(message, {
|
await window.MessageCache.saveMessage(message, {
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
});
|
});
|
||||||
if (!getIsInitialSync() && !this.get('active_at')) {
|
if (!getIsInitialContactSync() && !this.get('active_at')) {
|
||||||
this.set({ active_at: Date.now() });
|
this.set({ active_at: Date.now() });
|
||||||
await DataWriter.updateConversation(this.attributes);
|
await DataWriter.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,12 +125,13 @@ export class BackupsService {
|
||||||
this.api.clearCache();
|
this.api.clearCache();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public async downloadAndImport(
|
||||||
public async downloadAndImport(options: DownloadOptionsType): Promise<void> {
|
options: DownloadOptionsType
|
||||||
|
): Promise<{ wasBackupImported: boolean }> {
|
||||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||||
if (!backupDownloadPath) {
|
if (!backupDownloadPath) {
|
||||||
log.warn('backups.downloadAndImport: no backup download path, skipping');
|
log.warn('backups.downloadAndImport: no backup download path, skipping');
|
||||||
return;
|
return { wasBackupImported: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('backups.downloadAndImport: downloading...');
|
log.info('backups.downloadAndImport: downloading...');
|
||||||
|
@ -234,6 +235,7 @@ export class BackupsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`);
|
log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`);
|
||||||
|
return { wasBackupImported: hasBackup };
|
||||||
}
|
}
|
||||||
|
|
||||||
public retryDownload(): void {
|
public retryDownload(): void {
|
||||||
|
|
|
@ -30,11 +30,11 @@ import { AttachmentVariant } from '../types/Attachment';
|
||||||
// linking.
|
// linking.
|
||||||
let isInitialSync = false;
|
let isInitialSync = false;
|
||||||
|
|
||||||
export function setIsInitialSync(newValue: boolean): void {
|
export function setIsInitialContactSync(newValue: boolean): void {
|
||||||
log.info(`setIsInitialSync(${newValue})`);
|
log.info(`setIsInitialContactSync(${newValue})`);
|
||||||
isInitialSync = newValue;
|
isInitialSync = newValue;
|
||||||
}
|
}
|
||||||
export function getIsInitialSync(): boolean {
|
export function getIsInitialContactSync(): boolean {
|
||||||
return isInitialSync;
|
return isInitialSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +233,9 @@ async function doContactSync({
|
||||||
|
|
||||||
await window.storage.put('synced_at', Date.now());
|
await window.storage.put('synced_at', Date.now());
|
||||||
window.Whisper.events.trigger('contactSync:complete');
|
window.Whisper.events.trigger('contactSync:complete');
|
||||||
|
if (isInitialSync) {
|
||||||
|
isInitialSync = false;
|
||||||
|
}
|
||||||
window.SignalCI?.handleEvent('contactSync', isFullSync);
|
window.SignalCI?.handleEvent('contactSync', isFullSync);
|
||||||
|
|
||||||
log.info(`${logId}: done`);
|
log.info(`${logId}: done`);
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
// Copyright 2019 Signal Messenger, LLC
|
// Copyright 2019 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
|
||||||
|
import { MINUTE } from '../util/durations';
|
||||||
|
import { explodePromise } from '../util/explodePromise';
|
||||||
|
|
||||||
// Matching Whisper.events.trigger API
|
// Matching Whisper.events.trigger API
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function trigger(name: string, ...rest: Array<any>): void {
|
export function trigger(name: string, ...rest: Array<any>): void {
|
||||||
window.Whisper.events.trigger(name, ...rest);
|
window.Whisper.events.trigger(name, ...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const waitForEvent = (
|
||||||
|
eventName: string,
|
||||||
|
timeout: number = 2 * MINUTE
|
||||||
|
): Promise<void> =>
|
||||||
|
createTaskWithTimeout(
|
||||||
|
(event: string): Promise<void> => {
|
||||||
|
const { promise, resolve } = explodePromise<void>();
|
||||||
|
window.Whisper.events.once(event, () => resolve());
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
`waitForEvent:${eventName}`,
|
||||||
|
{ timeout }
|
||||||
|
)(eventName);
|
||||||
|
|
|
@ -77,6 +77,8 @@ describe('retries', function (this: Mocha.Suite) {
|
||||||
const { desktop, contacts } = bootstrap;
|
const { desktop, contacts } = bootstrap;
|
||||||
const [first] = contacts;
|
const [first] = contacts;
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
|
||||||
debug('send a sender key message without sending skdm first');
|
debug('send a sender key message without sending skdm first');
|
||||||
const firstDistributionId = await first.sendSenderKey(desktop, {
|
const firstDistributionId = await first.sendSenderKey(desktop, {
|
||||||
timestamp: bootstrap.getTimestamp(),
|
timestamp: bootstrap.getTimestamp(),
|
||||||
|
@ -87,7 +89,7 @@ describe('retries', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
debug('send a failing message');
|
debug('send a failing message');
|
||||||
const timestamp = bootstrap.getTimestamp();
|
const timestamp = bootstrap.getTimestamp();
|
||||||
await first.sendText(desktop, content, {
|
const firstMessageSend = first.sendText(desktop, content, {
|
||||||
distributionId: firstDistributionId,
|
distributionId: firstDistributionId,
|
||||||
sealed: true,
|
sealed: true,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -99,12 +101,18 @@ describe('retries', function (this: Mocha.Suite) {
|
||||||
});
|
});
|
||||||
|
|
||||||
debug('send same hello message, this time it should work');
|
debug('send same hello message, this time it should work');
|
||||||
await first.sendText(desktop, content, {
|
const secondMessageSend = first.sendText(desktop, content, {
|
||||||
distributionId: secondDistributionId,
|
distributionId: secondDistributionId,
|
||||||
sealed: true,
|
sealed: true,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debug('starting');
|
||||||
|
|
||||||
|
app = await bootstrap.startApp();
|
||||||
|
|
||||||
|
await Promise.all([firstMessageSend, secondMessageSend]);
|
||||||
|
|
||||||
debug('open conversation');
|
debug('open conversation');
|
||||||
const window = await app.getWindow();
|
const window = await app.getWindow();
|
||||||
const leftPane = window.locator('#LeftPane');
|
const leftPane = window.locator('#LeftPane');
|
||||||
|
|
|
@ -1188,7 +1188,13 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
await storage.put('identityKeyMap', identityKeyMap);
|
await storage.put('identityKeyMap', identityKeyMap);
|
||||||
await storage.put('registrationIdMap', registrationIdMap);
|
await storage.put('registrationIdMap', registrationIdMap);
|
||||||
|
|
||||||
await ourProfileKeyService.set(profileKey);
|
await ourProfileKeyService.set(profileKey);
|
||||||
|
const me = window.ConversationController.getOurConversationOrThrow();
|
||||||
|
await me.setProfileKey(Bytes.toBase64(profileKey), {
|
||||||
|
reason: 'registration',
|
||||||
|
});
|
||||||
|
|
||||||
if (userAgent) {
|
if (userAgent) {
|
||||||
await storage.put('userAgent', userAgent);
|
await storage.put('userAgent', userAgent);
|
||||||
}
|
}
|
||||||
|
@ -1351,7 +1357,7 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
async #registrationDone(): Promise<void> {
|
async #registrationDone(): Promise<void> {
|
||||||
log.info('registration done');
|
log.info('registration done');
|
||||||
this.dispatchEvent(new Event('registration'));
|
this.dispatchEvent(new Event('endRegistration'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPni(
|
async setPni(
|
||||||
|
|
|
@ -300,7 +300,6 @@ export default class MessageReceiver
|
||||||
#serverTrustRoot: Uint8Array;
|
#serverTrustRoot: Uint8Array;
|
||||||
#stoppingProcessing?: boolean;
|
#stoppingProcessing?: boolean;
|
||||||
#pniIdentityKeyCheckRequired?: boolean;
|
#pniIdentityKeyCheckRequired?: boolean;
|
||||||
#isAppReadyForProcessing: boolean = false;
|
|
||||||
|
|
||||||
constructor({ storage, serverTrustRoot }: MessageReceiverOptions) {
|
constructor({ storage, serverTrustRoot }: MessageReceiverOptions) {
|
||||||
super();
|
super();
|
||||||
|
@ -348,15 +347,6 @@ export default class MessageReceiver
|
||||||
maxSize: 30,
|
maxSize: 30,
|
||||||
processBatch: this.#cacheRemoveBatch.bind(this),
|
processBatch: this.#cacheRemoveBatch.bind(this),
|
||||||
});
|
});
|
||||||
|
|
||||||
window.Whisper.events.on('app-ready-for-processing', () => {
|
|
||||||
this.#isAppReadyForProcessing = true;
|
|
||||||
this.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.Whisper.events.on('online', () => {
|
|
||||||
this.reset();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAndResetProcessedCount(): number {
|
public getAndResetProcessedCount(): number {
|
||||||
|
@ -477,17 +467,12 @@ export default class MessageReceiver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(): void {
|
public startProcessingQueue(): void {
|
||||||
log.info('MessageReceiver.reset');
|
log.info('MessageReceiver.startProcessingQueue');
|
||||||
this.#count = 0;
|
this.#count = 0;
|
||||||
this.#isEmptied = false;
|
this.#isEmptied = false;
|
||||||
this.#stoppingProcessing = false;
|
this.#stoppingProcessing = false;
|
||||||
|
|
||||||
if (!this.#isAppReadyForProcessing) {
|
|
||||||
log.info('MessageReceiver.reset: not ready yet, returning early');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(this.#addCachedMessagesToQueue());
|
drop(this.#addCachedMessagesToQueue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,7 +492,6 @@ export default class MessageReceiver
|
||||||
public stopProcessing(): void {
|
public stopProcessing(): void {
|
||||||
log.info('MessageReceiver.stopProcessing');
|
log.info('MessageReceiver.stopProcessing');
|
||||||
this.#stoppingProcessing = true;
|
this.#stoppingProcessing = true;
|
||||||
this.#isAppReadyForProcessing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasEmptied(): boolean {
|
public hasEmptied(): boolean {
|
||||||
|
|
|
@ -554,7 +554,7 @@ export class SocketManager extends EventListener {
|
||||||
authenticated.abort();
|
authenticated.abort();
|
||||||
this.#dropAuthenticated(authenticated);
|
this.#dropAuthenticated(authenticated);
|
||||||
}
|
}
|
||||||
|
this.#markOffline();
|
||||||
this.#credentials = undefined;
|
this.#credentials = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
|
|
||||||
import type { EventHandler } from './EventTarget';
|
|
||||||
import EventTarget from './EventTarget';
|
|
||||||
import MessageReceiver from './MessageReceiver';
|
|
||||||
import type { ContactSyncEvent } from './messageReceiverEvents';
|
|
||||||
import MessageSender from './SendMessage';
|
|
||||||
import { assertDev } from '../util/assert';
|
|
||||||
import * as log from '../logging/log';
|
|
||||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
|
||||||
import * as Errors from '../types/errors';
|
|
||||||
|
|
||||||
class SyncRequestInner extends EventTarget {
|
|
||||||
#started = false;
|
|
||||||
|
|
||||||
contactSync?: boolean;
|
|
||||||
|
|
||||||
timeout: any;
|
|
||||||
|
|
||||||
oncontact: (event: ContactSyncEvent) => void;
|
|
||||||
|
|
||||||
timeoutMillis: number;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private receiver: MessageReceiver,
|
|
||||||
timeoutMillis?: number
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
if (!(receiver instanceof MessageReceiver)) {
|
|
||||||
throw new Error(
|
|
||||||
'Tried to construct a SyncRequest without MessageReceiver'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.oncontact = this.onContactSyncComplete.bind(this);
|
|
||||||
receiver.addEventListener('contactSync', this.oncontact);
|
|
||||||
|
|
||||||
this.timeoutMillis = timeoutMillis || 60000;
|
|
||||||
}
|
|
||||||
|
|
||||||
async start(): Promise<void> {
|
|
||||||
if (this.#started) {
|
|
||||||
assertDev(
|
|
||||||
false,
|
|
||||||
'SyncRequestInner: started more than once. Doing nothing'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#started = true;
|
|
||||||
|
|
||||||
if (window.ConversationController.areWePrimaryDevice()) {
|
|
||||||
log.warn('SyncRequest.start: We are primary device; returning early');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'SyncRequest created. Sending config, block, contact, and group requests...'
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
singleProtoJobQueue.add(
|
|
||||||
MessageSender.getRequestConfigurationSyncMessage()
|
|
||||||
),
|
|
||||||
singleProtoJobQueue.add(MessageSender.getRequestBlockSyncMessage()),
|
|
||||||
singleProtoJobQueue.add(MessageSender.getRequestContactSyncMessage()),
|
|
||||||
]);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
log.error(
|
|
||||||
'SyncRequest: Failed to add request jobs',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeout = setTimeout(this.onTimeout.bind(this), this.timeoutMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
onContactSyncComplete() {
|
|
||||||
this.contactSync = true;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.contactSync) {
|
|
||||||
this.dispatchEvent(new Event('success'));
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTimeout() {
|
|
||||||
if (this.contactSync) {
|
|
||||||
this.dispatchEvent(new Event('success'));
|
|
||||||
} else {
|
|
||||||
this.dispatchEvent(new Event('timeout'));
|
|
||||||
}
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
clearTimeout(this.timeout);
|
|
||||||
this.receiver.removeEventListener('contactsync', this.oncontact);
|
|
||||||
delete this.listeners;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SyncRequest {
|
|
||||||
#inner: SyncRequestInner;
|
|
||||||
|
|
||||||
addEventListener: (
|
|
||||||
name: 'success' | 'timeout',
|
|
||||||
handler: EventHandler
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
removeEventListener: (
|
|
||||||
name: 'success' | 'timeout',
|
|
||||||
handler: EventHandler
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
constructor(receiver: MessageReceiver, timeoutMillis?: number) {
|
|
||||||
const inner = new SyncRequestInner(receiver, timeoutMillis);
|
|
||||||
this.#inner = inner;
|
|
||||||
this.addEventListener = inner.addEventListener.bind(inner);
|
|
||||||
this.removeEventListener = inner.removeEventListener.bind(inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
start(): void {
|
|
||||||
void this.#inner.start();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,6 @@ import EventTarget from './EventTarget';
|
||||||
import AccountManager from './AccountManager';
|
import AccountManager from './AccountManager';
|
||||||
import MessageReceiver from './MessageReceiver';
|
import MessageReceiver from './MessageReceiver';
|
||||||
import utils from './Helpers';
|
import utils from './Helpers';
|
||||||
import SyncRequest from './SyncRequest';
|
|
||||||
import MessageSender from './SendMessage';
|
import MessageSender from './SendMessage';
|
||||||
import { Storage } from './Storage';
|
import { Storage } from './Storage';
|
||||||
import * as WebAPI from './WebAPI';
|
import * as WebAPI from './WebAPI';
|
||||||
|
@ -19,7 +18,6 @@ export type TextSecureType = {
|
||||||
EventTarget: typeof EventTarget;
|
EventTarget: typeof EventTarget;
|
||||||
MessageReceiver: typeof MessageReceiver;
|
MessageReceiver: typeof MessageReceiver;
|
||||||
MessageSender: typeof MessageSender;
|
MessageSender: typeof MessageSender;
|
||||||
SyncRequest: typeof SyncRequest;
|
|
||||||
WebAPI: typeof WebAPI;
|
WebAPI: typeof WebAPI;
|
||||||
WebSocketResource: typeof WebSocketResource;
|
WebSocketResource: typeof WebSocketResource;
|
||||||
|
|
||||||
|
@ -35,7 +33,6 @@ export const textsecure: TextSecureType = {
|
||||||
EventTarget,
|
EventTarget,
|
||||||
MessageReceiver,
|
MessageReceiver,
|
||||||
MessageSender,
|
MessageSender,
|
||||||
SyncRequest,
|
|
||||||
WebAPI,
|
WebAPI,
|
||||||
WebSocketResource,
|
WebSocketResource,
|
||||||
};
|
};
|
||||||
|
|
32
ts/textsecure/syncRequests.ts
Normal file
32
ts/textsecure/syncRequests.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { waitForEvent } from '../shims/events';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
|
import MessageSender from './SendMessage';
|
||||||
|
import { toLogFormat } from '../types/errors';
|
||||||
|
|
||||||
|
export async function sendSyncRequests(
|
||||||
|
timeout?: number
|
||||||
|
): Promise<{ contactSyncComplete: Promise<void> }> {
|
||||||
|
const contactSyncComplete = waitForEvent('contactSync:complete', timeout);
|
||||||
|
|
||||||
|
log.info('sendSyncRequests: sending sync requests');
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
singleProtoJobQueue.add(MessageSender.getRequestContactSyncMessage()),
|
||||||
|
singleProtoJobQueue.add(
|
||||||
|
MessageSender.getRequestConfigurationSyncMessage()
|
||||||
|
),
|
||||||
|
singleProtoJobQueue.add(MessageSender.getRequestBlockSyncMessage()),
|
||||||
|
]);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
log.error(
|
||||||
|
'sendSyncRequests: failed to send sync requests',
|
||||||
|
toLogFormat(error)
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return { contactSyncComplete };
|
||||||
|
}
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
|
@ -211,6 +211,8 @@ export type StorageAccessType = {
|
||||||
// The `firstAppVersion` present on an BackupInfo from an imported backup.
|
// The `firstAppVersion` present on an BackupInfo from an imported backup.
|
||||||
restoredBackupFirstAppVersion: string;
|
restoredBackupFirstAppVersion: string;
|
||||||
|
|
||||||
|
postRegistrationSyncsStatus: 'incomplete' | 'complete';
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
'challenge:retry-message-ids': never;
|
'challenge:retry-message-ids': never;
|
||||||
nextSignedKeyRotationTime: number;
|
nextSignedKeyRotationTime: number;
|
||||||
|
|
|
@ -52,6 +52,7 @@ import type {
|
||||||
} from './preload';
|
} from './preload';
|
||||||
import type { SystemTraySetting } from '../types/SystemTraySetting';
|
import type { SystemTraySetting } from '../types/SystemTraySetting';
|
||||||
import { drop } from './drop';
|
import { drop } from './drop';
|
||||||
|
import { sendSyncRequests } from '../textsecure/syncRequests';
|
||||||
|
|
||||||
type SentMediaQualityType = 'standard' | 'high';
|
type SentMediaQualityType = 'standard' | 'high';
|
||||||
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||||
|
@ -487,15 +488,12 @@ export function createIPCEvents(
|
||||||
},
|
},
|
||||||
|
|
||||||
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
|
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
|
||||||
syncRequest: () =>
|
syncRequest: async () => {
|
||||||
new Promise<void>((resolve, reject) => {
|
const { contactSyncComplete } = await sendSyncRequests(
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
5 * durations.MINUTE
|
||||||
const syncRequest = window.getSyncRequest(FIVE_MINUTES);
|
);
|
||||||
syncRequest.addEventListener('success', () => resolve());
|
return contactSyncComplete;
|
||||||
syncRequest.addEventListener('timeout', () =>
|
},
|
||||||
reject(new Error('timeout'))
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
getLastSyncTime: () => window.storage.get('synced_at'),
|
getLastSyncTime: () => window.storage.get('synced_at'),
|
||||||
setLastSyncTime: value => window.storage.put('synced_at', value),
|
setLastSyncTime: value => window.storage.put('synced_at', value),
|
||||||
getUniversalExpireTimer: () => universalExpireTimer.get(),
|
getUniversalExpireTimer: () => universalExpireTimer.get(),
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -38,7 +38,6 @@ import type { ConfirmationDialog } from './components/ConfirmationDialog';
|
||||||
import type { SignalProtocolStore } from './SignalProtocolStore';
|
import type { SignalProtocolStore } from './SignalProtocolStore';
|
||||||
import type { SocketStatus } from './types/SocketStatus';
|
import type { SocketStatus } from './types/SocketStatus';
|
||||||
import type { ScreenShareStatus } from './types/Calling';
|
import type { ScreenShareStatus } from './types/Calling';
|
||||||
import type SyncRequest from './textsecure/SyncRequest';
|
|
||||||
import type { MessageCache } from './services/MessageCache';
|
import type { MessageCache } from './services/MessageCache';
|
||||||
import type { StateType } from './state/reducer';
|
import type { StateType } from './state/reducer';
|
||||||
import type { Address } from './types/Address';
|
import type { Address } from './types/Address';
|
||||||
|
@ -203,7 +202,6 @@ declare global {
|
||||||
getSfuUrl: () => string;
|
getSfuUrl: () => string;
|
||||||
getIceServerOverride: () => string;
|
getIceServerOverride: () => string;
|
||||||
getSocketStatus: () => SocketStatus;
|
getSocketStatus: () => SocketStatus;
|
||||||
getSyncRequest: (timeoutMillis?: number) => SyncRequest;
|
|
||||||
getTitle: () => string;
|
getTitle: () => string;
|
||||||
waitForEmptyEventQueue: () => Promise<void>;
|
waitForEmptyEventQueue: () => Promise<void>;
|
||||||
getVersion: () => string;
|
getVersion: () => string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue