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: {
|
||||
get: key => storageMap.get(key),
|
||||
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 { HashType } from './types/Crypto';
|
||||
import { getCountryCode } from './types/PhoneNumber';
|
||||
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
||||
|
||||
export type ConfigKeyType =
|
||||
| 'desktop.calling.ringrtcAdmFull'
|
||||
|
@ -135,13 +136,23 @@ export const _refreshRemoteConfig = async (
|
|||
};
|
||||
}, {});
|
||||
|
||||
// If remote configuration fetch worked - we are not expired anymore.
|
||||
if (
|
||||
!getValue('desktop.clientExpiration') &&
|
||||
window.storage.get('remoteBuildExpiration') != null
|
||||
) {
|
||||
log.warn('Remote Config: clearing remote expiration on successful fetch');
|
||||
const remoteExpirationValue = getValue('desktop.clientExpiration');
|
||||
if (!remoteExpirationValue) {
|
||||
// If remote configuration fetch worked - we are not expired anymore.
|
||||
if (window.storage.get('remoteBuildExpiration') != null) {
|
||||
log.warn('Remote Config: clearing remote expiration on successful fetch');
|
||||
}
|
||||
await window.storage.remove('remoteBuildExpiration');
|
||||
} else {
|
||||
const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(
|
||||
remoteExpirationValue
|
||||
);
|
||||
if (remoteBuildExpirationTimestamp) {
|
||||
await window.storage.put(
|
||||
'remoteBuildExpiration',
|
||||
remoteBuildExpirationTimestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 { notificationService } from './services/notifications';
|
||||
import { areWeASubscriberService } from './services/areWeASubscriber';
|
||||
import { onContactSync, setIsInitialSync } from './services/contactSync';
|
||||
import { onContactSync, setIsInitialContactSync } from './services/contactSync';
|
||||
import { startTimeTravelDetector } from './util/startTimeTravelDetector';
|
||||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||
import { LatestQueue } from './util/LatestQueue';
|
||||
|
@ -206,6 +206,8 @@ import {
|
|||
import { postSaveUpdates } from './util/cleanup';
|
||||
import { handleDataMessage } from './messages/handleDataMessage';
|
||||
import { MessageModel } from './models/messages';
|
||||
import { waitForEvent } from './shims/events';
|
||||
import { sendSyncRequests } from './textsecure/syncRequests';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -402,7 +404,6 @@ export async function startApp(): Promise<void> {
|
|||
};
|
||||
|
||||
let accountManager: AccountManager;
|
||||
let isInRegistration = false;
|
||||
window.getAccountManager = () => {
|
||||
if (accountManager) {
|
||||
return accountManager;
|
||||
|
@ -413,19 +414,23 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
accountManager = new window.textsecure.AccountManager(server);
|
||||
accountManager.addEventListener('startRegistration', () => {
|
||||
isInRegistration = true;
|
||||
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 = explodePromise();
|
||||
registrationCompleted = explodePromise();
|
||||
});
|
||||
accountManager.addEventListener('registration', () => {
|
||||
isInRegistration = false;
|
||||
|
||||
accountManager.addEventListener('endRegistration', () => {
|
||||
window.Whisper.events.trigger('userChanged', false);
|
||||
|
||||
drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete'));
|
||||
registrationCompleted?.resolve();
|
||||
drop(Registration.markDone());
|
||||
log.info('dispatching registration event');
|
||||
window.Whisper.events.trigger('registration_done');
|
||||
});
|
||||
return accountManager;
|
||||
};
|
||||
|
@ -1489,18 +1494,6 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
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();
|
||||
render(
|
||||
window.Signal.State.Roots.createApp(window.reduxStore),
|
||||
|
@ -1527,7 +1520,7 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
|
||||
if (isCoreDataValid && Registration.everDone()) {
|
||||
drop(connect());
|
||||
idleDetector.start();
|
||||
if (window.storage.get('backupDownloadPath')) {
|
||||
window.reduxActions.installer.showBackupImport();
|
||||
} else {
|
||||
|
@ -1583,25 +1576,17 @@ export async function startApp(): Promise<void> {
|
|||
resolveOnAppView = undefined;
|
||||
}
|
||||
|
||||
drop(afterStart());
|
||||
setupNetworkChangeListeners();
|
||||
}
|
||||
|
||||
async function afterStart() {
|
||||
strictAssert(messageReceiver, 'messageReceiver must be initialized');
|
||||
function setupNetworkChangeListeners() {
|
||||
strictAssert(server, 'server must be initialized');
|
||||
|
||||
log.info('afterStart(): emitting app-ready-for-processing');
|
||||
window.Whisper.events.trigger('app-ready-for-processing');
|
||||
|
||||
const onOnline = () => {
|
||||
log.info('background: online');
|
||||
|
||||
// Do not attempt to connect while expired or in-the-middle of
|
||||
// registration
|
||||
if (!remotelyExpired && !isInRegistration) {
|
||||
drop(connect());
|
||||
}
|
||||
drop(afterAuthSocketConnect());
|
||||
};
|
||||
|
||||
window.Whisper.events.on('online', onOnline);
|
||||
|
||||
const onOffline = () => {
|
||||
|
@ -1611,7 +1596,7 @@ export async function startApp(): Promise<void> {
|
|||
const hasAppEverBeenRegistered = Registration.everDone();
|
||||
|
||||
log.info('background: offline', {
|
||||
connectCount,
|
||||
authSocketConnectCount,
|
||||
hasInitialLoadCompleted,
|
||||
appView,
|
||||
hasAppEverBeenRegistered,
|
||||
|
@ -1620,7 +1605,11 @@ export async function startApp(): Promise<void> {
|
|||
drop(challengeHandler?.onOffline());
|
||||
drop(AttachmentDownloadManager.stop());
|
||||
drop(AttachmentBackupManager.stop());
|
||||
drop(messageReceiver?.drain());
|
||||
|
||||
if (messageReceiver) {
|
||||
drop(messageReceiver.drain());
|
||||
server?.unregisterRequestHandler(messageReceiver);
|
||||
}
|
||||
|
||||
if (hasAppEverBeenRegistered) {
|
||||
const state = window.reduxStore.getState();
|
||||
|
@ -1653,14 +1642,171 @@ export async function startApp(): Promise<void> {
|
|||
} else if (server.isOnline() === false) {
|
||||
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');
|
||||
if (backupDownloadPath) {
|
||||
tapToViewMessagesDeletionService.pause();
|
||||
|
||||
// Download backup before enabling request handler and storage service
|
||||
try {
|
||||
await backupsService.downloadAndImport({
|
||||
const { wasBackupImported } = await backupsService.downloadAndImport({
|
||||
onProgress: (backupStep, currentBytes, totalBytes) => {
|
||||
window.reduxActions.installer.updateBackupImportProgress({
|
||||
backupStep,
|
||||
|
@ -1670,36 +1816,121 @@ export async function startApp(): Promise<void> {
|
|||
},
|
||||
});
|
||||
|
||||
log.info('afterStart: backup download attempt completed, resolving');
|
||||
backupReady.resolve();
|
||||
log.info('afterAppStart: backup download attempt completed, resolving');
|
||||
backupReady.resolve({ wasBackupImported });
|
||||
} catch (error) {
|
||||
log.error('afterStart: backup download failed, rejecting');
|
||||
log.error('afterAppStart: backup download failed, rejecting');
|
||||
backupReady.reject(error);
|
||||
throw error;
|
||||
} finally {
|
||||
tapToViewMessagesDeletionService.resume();
|
||||
}
|
||||
} else {
|
||||
backupReady.resolve();
|
||||
backupReady.resolve({ wasBackupImported: false });
|
||||
}
|
||||
|
||||
server.registerRequestHandler(messageReceiver);
|
||||
drop(runStorageService({ reason: 'afterStart' }));
|
||||
|
||||
// Opposite of `messageReceiver.stopProcessing`
|
||||
messageReceiver.reset();
|
||||
return backupReady.promise;
|
||||
}
|
||||
|
||||
window.getSyncRequest = (timeoutMillis?: number) => {
|
||||
strictAssert(messageReceiver, 'MessageReceiver not initialized');
|
||||
function afterEveryLinkedStartup() {
|
||||
log.info('afterAuthSocketConnect/afterEveryLinkedStartup');
|
||||
|
||||
const syncRequest = new window.textsecure.SyncRequest(
|
||||
messageReceiver,
|
||||
timeoutMillis
|
||||
);
|
||||
syncRequest.start();
|
||||
return syncRequest;
|
||||
};
|
||||
// Note: we always have to register our capabilities all at once, so we do this
|
||||
// after connect on every startup
|
||||
drop(registerCapabilities());
|
||||
drop(ensureAEP());
|
||||
drop(maybeQueueDeviceNameFetch());
|
||||
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() {
|
||||
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);
|
||||
|
||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||
|
@ -2215,7 +2101,6 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
log.info('manualConnect: calling connect()');
|
||||
enqueueReconnectToWebSocket();
|
||||
drop(connect());
|
||||
}
|
||||
|
||||
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
||||
|
|
|
@ -188,7 +188,7 @@ import { explodePromise } from '../util/explodePromise';
|
|||
import { getCallHistorySelector } from '../state/selectors/callHistory';
|
||||
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
||||
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
||||
import { getIsInitialSync } from '../services/contactSync';
|
||||
import { getIsInitialContactSync } from '../services/contactSync';
|
||||
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
|
||||
import { cleanupMessages } from '../util/cleanup';
|
||||
import { MessageModel } from './messages';
|
||||
|
@ -2348,7 +2348,7 @@ export class ConversationModel extends window.Backbone
|
|||
await window.MessageCache.saveMessage(message, {
|
||||
forceSave: true,
|
||||
});
|
||||
if (!getIsInitialSync() && !this.get('active_at')) {
|
||||
if (!getIsInitialContactSync() && !this.get('active_at')) {
|
||||
this.set({ active_at: Date.now() });
|
||||
await DataWriter.updateConversation(this.attributes);
|
||||
}
|
||||
|
|
|
@ -125,12 +125,13 @@ export class BackupsService {
|
|||
this.api.clearCache();
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadAndImport(options: DownloadOptionsType): Promise<void> {
|
||||
public async downloadAndImport(
|
||||
options: DownloadOptionsType
|
||||
): Promise<{ wasBackupImported: boolean }> {
|
||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||
if (!backupDownloadPath) {
|
||||
log.warn('backups.downloadAndImport: no backup download path, skipping');
|
||||
return;
|
||||
return { wasBackupImported: false };
|
||||
}
|
||||
|
||||
log.info('backups.downloadAndImport: downloading...');
|
||||
|
@ -234,6 +235,7 @@ export class BackupsService {
|
|||
}
|
||||
|
||||
log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`);
|
||||
return { wasBackupImported: hasBackup };
|
||||
}
|
||||
|
||||
public retryDownload(): void {
|
||||
|
|
|
@ -30,11 +30,11 @@ import { AttachmentVariant } from '../types/Attachment';
|
|||
// linking.
|
||||
let isInitialSync = false;
|
||||
|
||||
export function setIsInitialSync(newValue: boolean): void {
|
||||
log.info(`setIsInitialSync(${newValue})`);
|
||||
export function setIsInitialContactSync(newValue: boolean): void {
|
||||
log.info(`setIsInitialContactSync(${newValue})`);
|
||||
isInitialSync = newValue;
|
||||
}
|
||||
export function getIsInitialSync(): boolean {
|
||||
export function getIsInitialContactSync(): boolean {
|
||||
return isInitialSync;
|
||||
}
|
||||
|
||||
|
@ -233,6 +233,9 @@ async function doContactSync({
|
|||
|
||||
await window.storage.put('synced_at', Date.now());
|
||||
window.Whisper.events.trigger('contactSync:complete');
|
||||
if (isInitialSync) {
|
||||
isInitialSync = false;
|
||||
}
|
||||
window.SignalCI?.handleEvent('contactSync', isFullSync);
|
||||
|
||||
log.info(`${logId}: done`);
|
||||
|
|
|
@ -1,8 +1,26 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// 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
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function trigger(name: string, ...rest: Array<any>): void {
|
||||
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 [first] = contacts;
|
||||
|
||||
await app.close();
|
||||
|
||||
debug('send a sender key message without sending skdm first');
|
||||
const firstDistributionId = await first.sendSenderKey(desktop, {
|
||||
timestamp: bootstrap.getTimestamp(),
|
||||
|
@ -87,7 +89,7 @@ describe('retries', function (this: Mocha.Suite) {
|
|||
|
||||
debug('send a failing message');
|
||||
const timestamp = bootstrap.getTimestamp();
|
||||
await first.sendText(desktop, content, {
|
||||
const firstMessageSend = first.sendText(desktop, content, {
|
||||
distributionId: firstDistributionId,
|
||||
sealed: true,
|
||||
timestamp,
|
||||
|
@ -99,12 +101,18 @@ describe('retries', function (this: Mocha.Suite) {
|
|||
});
|
||||
|
||||
debug('send same hello message, this time it should work');
|
||||
await first.sendText(desktop, content, {
|
||||
const secondMessageSend = first.sendText(desktop, content, {
|
||||
distributionId: secondDistributionId,
|
||||
sealed: true,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
debug('starting');
|
||||
|
||||
app = await bootstrap.startApp();
|
||||
|
||||
await Promise.all([firstMessageSend, secondMessageSend]);
|
||||
|
||||
debug('open conversation');
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
|
|
@ -1188,7 +1188,13 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
await storage.put('identityKeyMap', identityKeyMap);
|
||||
await storage.put('registrationIdMap', registrationIdMap);
|
||||
|
||||
await ourProfileKeyService.set(profileKey);
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
await me.setProfileKey(Bytes.toBase64(profileKey), {
|
||||
reason: 'registration',
|
||||
});
|
||||
|
||||
if (userAgent) {
|
||||
await storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
@ -1351,7 +1357,7 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
async #registrationDone(): Promise<void> {
|
||||
log.info('registration done');
|
||||
this.dispatchEvent(new Event('registration'));
|
||||
this.dispatchEvent(new Event('endRegistration'));
|
||||
}
|
||||
|
||||
async setPni(
|
||||
|
|
|
@ -300,7 +300,6 @@ export default class MessageReceiver
|
|||
#serverTrustRoot: Uint8Array;
|
||||
#stoppingProcessing?: boolean;
|
||||
#pniIdentityKeyCheckRequired?: boolean;
|
||||
#isAppReadyForProcessing: boolean = false;
|
||||
|
||||
constructor({ storage, serverTrustRoot }: MessageReceiverOptions) {
|
||||
super();
|
||||
|
@ -348,15 +347,6 @@ export default class MessageReceiver
|
|||
maxSize: 30,
|
||||
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 {
|
||||
|
@ -477,17 +467,12 @@ export default class MessageReceiver
|
|||
);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
log.info('MessageReceiver.reset');
|
||||
public startProcessingQueue(): void {
|
||||
log.info('MessageReceiver.startProcessingQueue');
|
||||
this.#count = 0;
|
||||
this.#isEmptied = false;
|
||||
this.#stoppingProcessing = false;
|
||||
|
||||
if (!this.#isAppReadyForProcessing) {
|
||||
log.info('MessageReceiver.reset: not ready yet, returning early');
|
||||
return;
|
||||
}
|
||||
|
||||
drop(this.#addCachedMessagesToQueue());
|
||||
}
|
||||
|
||||
|
@ -507,7 +492,6 @@ export default class MessageReceiver
|
|||
public stopProcessing(): void {
|
||||
log.info('MessageReceiver.stopProcessing');
|
||||
this.#stoppingProcessing = true;
|
||||
this.#isAppReadyForProcessing = false;
|
||||
}
|
||||
|
||||
public hasEmptied(): boolean {
|
||||
|
|
|
@ -554,7 +554,7 @@ export class SocketManager extends EventListener {
|
|||
authenticated.abort();
|
||||
this.#dropAuthenticated(authenticated);
|
||||
}
|
||||
|
||||
this.#markOffline();
|
||||
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 MessageReceiver from './MessageReceiver';
|
||||
import utils from './Helpers';
|
||||
import SyncRequest from './SyncRequest';
|
||||
import MessageSender from './SendMessage';
|
||||
import { Storage } from './Storage';
|
||||
import * as WebAPI from './WebAPI';
|
||||
|
@ -19,7 +18,6 @@ export type TextSecureType = {
|
|||
EventTarget: typeof EventTarget;
|
||||
MessageReceiver: typeof MessageReceiver;
|
||||
MessageSender: typeof MessageSender;
|
||||
SyncRequest: typeof SyncRequest;
|
||||
WebAPI: typeof WebAPI;
|
||||
WebSocketResource: typeof WebSocketResource;
|
||||
|
||||
|
@ -35,7 +33,6 @@ export const textsecure: TextSecureType = {
|
|||
EventTarget,
|
||||
MessageReceiver,
|
||||
MessageSender,
|
||||
SyncRequest,
|
||||
WebAPI,
|
||||
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.
|
||||
restoredBackupFirstAppVersion: string;
|
||||
|
||||
postRegistrationSyncsStatus: 'incomplete' | 'complete';
|
||||
|
||||
// Deprecated
|
||||
'challenge:retry-message-ids': never;
|
||||
nextSignedKeyRotationTime: number;
|
||||
|
|
|
@ -52,6 +52,7 @@ import type {
|
|||
} from './preload';
|
||||
import type { SystemTraySetting } from '../types/SystemTraySetting';
|
||||
import { drop } from './drop';
|
||||
import { sendSyncRequests } from '../textsecure/syncRequests';
|
||||
|
||||
type SentMediaQualityType = 'standard' | 'high';
|
||||
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||
|
@ -487,15 +488,12 @@ export function createIPCEvents(
|
|||
},
|
||||
|
||||
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
|
||||
syncRequest: () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||
const syncRequest = window.getSyncRequest(FIVE_MINUTES);
|
||||
syncRequest.addEventListener('success', () => resolve());
|
||||
syncRequest.addEventListener('timeout', () =>
|
||||
reject(new Error('timeout'))
|
||||
);
|
||||
}),
|
||||
syncRequest: async () => {
|
||||
const { contactSyncComplete } = await sendSyncRequests(
|
||||
5 * durations.MINUTE
|
||||
);
|
||||
return contactSyncComplete;
|
||||
},
|
||||
getLastSyncTime: () => window.storage.get('synced_at'),
|
||||
setLastSyncTime: value => window.storage.put('synced_at', value),
|
||||
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 { SocketStatus } from './types/SocketStatus';
|
||||
import type { ScreenShareStatus } from './types/Calling';
|
||||
import type SyncRequest from './textsecure/SyncRequest';
|
||||
import type { MessageCache } from './services/MessageCache';
|
||||
import type { StateType } from './state/reducer';
|
||||
import type { Address } from './types/Address';
|
||||
|
@ -203,7 +202,6 @@ declare global {
|
|||
getSfuUrl: () => string;
|
||||
getIceServerOverride: () => string;
|
||||
getSocketStatus: () => SocketStatus;
|
||||
getSyncRequest: (timeoutMillis?: number) => SyncRequest;
|
||||
getTitle: () => string;
|
||||
waitForEmptyEventQueue: () => Promise<void>;
|
||||
getVersion: () => string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue